From e0f8aec5c6b7614e0f4376a685353952d60c36be Mon Sep 17 00:00:00 2001 From: Solganis Date: Mon, 12 Oct 2026 18:40:06 +0300 Subject: [PATCH] fix: draw the held weapon at its own size and place in widescreen --- .../files/runtime/include/gpu_ws_hud_scale.h | 20 +++ framework/patches/runtime/include/gpu.h.patch | 16 +- framework/patches/runtime/src/gpu.c.patch | 162 +++++++++++------- src/game/render/hud_widgets.cpp | 9 + tests/contracts/test_hud_widget_contract.py | 43 ++++- tests/unit/gpu_ws_hud_scale_test.cpp | 42 +++++ 6 files changed, 223 insertions(+), 69 deletions(-) diff --git a/framework/files/runtime/include/gpu_ws_hud_scale.h b/framework/files/runtime/include/gpu_ws_hud_scale.h index c529aa9..be17eaf 100644 --- a/framework/files/runtime/include/gpu_ws_hud_scale.h +++ b/framework/files/runtime/include/gpu_ws_hud_scale.h @@ -31,6 +31,26 @@ static inline int32_t psx_ws_hud_thirds_centre(int32_t x, int32_t w, int32_t str return w == strip && 3 * centre > 4 * screen_w && 3 * x <= 2 * screen_w ? screen_w : centre; } +/* The column a held weapon's sprite [x, x + w) is squashed about. In the middle third of the screen that is the + * screen's centre, as for any sprite there. In an outer third it slides toward that edge and is on it when the sprite + * touches it: a picture the screen crops stays cropped by the screen, and nothing jumps as the sprite crosses a third. + * A sprite too wide to slide inside an outer third, one over five twelfths of the screen, slides from the centre on. */ +static inline int32_t psx_ws_hud_held_pivot(int32_t x, int32_t w, int32_t screen_w) { + int64_t centre, start, reach, gone; + int mirrored; + if (w <= 0 || screen_w <= 0 || w >= screen_w) return screen_w / 2; + centre = 2 * (int64_t)x + w; + mirrored = centre < screen_w; + if (mirrored) centre = 2 * (int64_t)screen_w - centre; + start = 12 * (int64_t)w <= 5 * (int64_t)screen_w ? 4 * (int64_t)screen_w : 3 * (int64_t)screen_w; /* in sixths of a pixel */ + gone = 3 * centre - start; + if (gone <= 0) return screen_w / 2; + reach = 3 * (2 * (int64_t)screen_w - w) - start; + if (gone > reach) gone = reach; + centre = ((int64_t)screen_w * (reach + gone) + reach) / (2 * reach); + return (int32_t)(mirrored ? screen_w - centre : centre); +} + /* Shrinks a screen-space widget toward the edge or corner of the screen it * sits at: thirds of the width and of the height pick the pivot, as the stock * horizontal squash does. A counter whose pieces all centre in one third keeps diff --git a/framework/patches/runtime/include/gpu.h.patch b/framework/patches/runtime/include/gpu.h.patch index 25d98c3..6ae0061 100644 --- a/framework/patches/runtime/include/gpu.h.patch +++ b/framework/patches/runtime/include/gpu.h.patch @@ -1,5 +1,5 @@ diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h -index 11b29fe121b12ffe7b453fde551c274d99425a1e..fad42ec530bb44fb305f34317c46958e1d126ace 100644 +index 11b29fe121b12ffe7b453fde551c274d99425a1e..a90850743edafc6c5e0e66809b6a03cc8e1dc44e 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -1,4 +1,4 @@ @@ -144,7 +144,7 @@ index 11b29fe121b12ffe7b453fde551c274d99425a1e..fad42ec530bb44fb305f34317c46958e /* Widescreen proportion correction (active only when aspect != 4:3 and the * game's [widescreen] block opts in — see config_loader.h). Tagged * character/billboard prims are re-squashed around their projected anchor; -@@ -126,6 +255,39 @@ +@@ -126,6 +255,43 @@ * (render the wider FOV into a wider frame, present 1:1; GTE not squashed). */ void gpu_ws_configure(int aspect_num, int aspect_den, uint32_t sprite_anchor_addr, int hud_sprt_squash, int mode); @@ -181,10 +181,14 @@ index 11b29fe121b12ffe7b453fde551c274d99425a1e..fad42ec530bb44fb305f34317c46958e +void gpu_ws_set_hud_widgets(uint32_t table_pointer, int layers, const int32_t *boxes, int count); +/* A game whose text printer draws a line as one picture `wide` across, the words from its left end. */ +void gpu_ws_set_hud_text_strip(int wide); ++/* The texture page, by its two indices, on which a game keeps the picture of the weapon in the player's hands. A sprite ++ * of that page in a HUD layer is the weapon: it keeps its size whatever the HUD size, and all of it in one list is ++ * squashed about one column, psx_ws_hud_held_pivot of its first sprite. A negative index names none. */ ++void gpu_ws_set_hud_held_page(int page_x, int page_y); /* [widescreen] full_2d: opt a pure-2D sprite game into the widescreen present * path (treat every in-game frame as gameplay, since it never tags 3D prims). */ void gpu_ws_set_full_2d(int on); -@@ -186,6 +348,13 @@ +@@ -186,6 +352,13 @@ * full-2D menu/title screen), so the squash is suppressed and content drawn * pixel-native. The present path uses the same predicate to pillarbox. */ int gpu_ws_present_native_43(void); @@ -198,7 +202,7 @@ index 11b29fe121b12ffe7b453fde551c274d99425a1e..fad42ec530bb44fb305f34317c46958e /* Per-side X cull-margin (screen/world units) emitted into the game's draw- * cull immediates by the recompiler ([widescreen.cull]); 0 unless stretching. */ int psx_ws_x_margin(void); -@@ -240,6 +409,8 @@ +@@ -240,6 +413,8 @@ int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla, uint32_t *out); uint32_t psx_ws_angle_widen(uint32_t vanilla); @@ -207,7 +211,7 @@ index 11b29fe121b12ffe7b453fde551c274d99425a1e..fad42ec530bb44fb305f34317c46958e int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out); uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla, uint32_t object, int32_t x, int32_t z, -@@ -263,6 +434,9 @@ +@@ -263,6 +438,9 @@ * widen for the paired `bltz maxSX` reject. Identity at 4:3. */ int psx_ws_cull_slti(uint32_t sx, uint32_t imm); int psx_ws_cull_slti_lower(uint32_t sx, uint32_t imm); @@ -217,7 +221,7 @@ index 11b29fe121b12ffe7b453fde551c274d99425a1e..fad42ec530bb44fb305f34317c46958e int psx_ws_cull_bltz(uint32_t v); int psx_ws_cull_vxrange(uint32_t x, uint32_t imm); /* True if a run of instruction words carries the screen-extent reject signature -@@ -318,6 +492,10 @@ +@@ -318,6 +496,10 @@ * final screenX store to squash it around the screen centre (identity at 4:3). * Pulls far backdrop pieces in from past the 320px edge to cover the 16:9 FOV. */ int psx_ws_backdrop_x(int x); diff --git a/framework/patches/runtime/src/gpu.c.patch b/framework/patches/runtime/src/gpu.c.patch index 6068237..9fad432 100644 --- a/framework/patches/runtime/src/gpu.c.patch +++ b/framework/patches/runtime/src/gpu.c.patch @@ -1,5 +1,5 @@ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c -index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e973793c3 100644 +index 6d81477285f786cdd3658d14c424d4bfbab5d809..1e2a0488eba569e12783e4575b646fed9a4053ab 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -11,6 +11,7 @@ @@ -68,7 +68,15 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e static WsTag ws_tags[WS_TAG_BUCKETS]; static uint32_t ws_last_tag_stamp = (uint32_t)-1000; /* frame of newest tag */ static uint32_t ws_last_3d_stamp = (uint32_t)-1000; /* frame of newest shaded prim (diagnostic) */ -@@ -177,6 +214,8 @@ +@@ -124,6 +161,7 @@ + + /* Forward decls: defined later but used by psx_ws_backdrop_x above them. */ + static int32_t ws_scale_about(int32_t x, int32_t ax); ++static int ws_hud_held_rect(int32_t *x, int w); + static int32_t ws_disp_w(void); + static void ws_clear_all_reveal_margins(void); + +@@ -177,6 +215,8 @@ * intro-cutscene flicker). Only a genuine full-2D screen — no GTE projection for * this many consecutive frames (save/options/memory-card) — reverts to 4:3. */ #define WS_GTE_GAME_MODE_HYSTERESIS 45u @@ -77,7 +85,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e void gpu_ws_set_gte_game_mode(int on) { ws_gte_game_mode_cfg = on ? 1 : 0; } void gpu_ws_set_gameplay_state_gate(uint32_t addr, const uint32_t *values, int nvalues) { -@@ -240,6 +279,7 @@ +@@ -240,6 +280,7 @@ void psx_ws_note_gte_project(int nverts) { uint32_t f = (uint32_t)s_frame_count; @@ -85,7 +93,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e if (f != ws_gte_frame) { if (f == ws_gte_frame + 1u) ws_gte_prev_verts = ws_gte_count; else ws_gte_prev_verts = 0; -@@ -262,14 +302,22 @@ +@@ -262,14 +303,22 @@ if (env < 0) { const char *e = getenv("PSX_WS_FORCE_2D"); env = (e && e[0] == '1') ? 1 : 0; } return ws_full_2d || env; } @@ -111,7 +119,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* True when the current frame is presented at native 4:3 (NOT stretched), so * ALL squash must be suppressed and the content rendered pixel-native: -@@ -304,9 +352,19 @@ +@@ -304,9 +353,19 @@ static uint32_t s_ws_fmv_frame_cache = 0xFFFFFFFFu; static int s_ws_fmv_cached = 0; @@ -133,7 +141,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e if (ws_2d_only_scene()) return 1; /* 2D-only gameplay scene */ uint32_t f = (uint32_t)s_frame_count; if (f != s_ws_fmv_frame_cache) { -@@ -315,6 +373,14 @@ +@@ -315,6 +374,14 @@ s_ws_fmv_cached = di.depth24 || mdec_recently_active(WS_FMV_HYSTERESIS); } return s_ws_fmv_cached; @@ -148,7 +156,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Squash applies only when configured AND the frame is being stretched. */ -@@ -599,6 +665,38 @@ +@@ -599,6 +666,38 @@ if (widened > ws_angle_max_widened) ws_angle_max_widened = widened; return widened; @@ -187,7 +195,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out) { -@@ -1303,6 +1401,23 @@ +@@ -1303,6 +1402,23 @@ return ((int32_t)sx < bound - psx_ws_x_margin()) ? 1 : 0; } @@ -211,7 +219,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Signed left-edge widen for the funnel's `bltz maxSX, reject`: reject only * when the prim ends left of the REVEALED edge (maxSX < -margin). Returns the * branch predicate. Identity at margin 0 (4:3). */ -@@ -1403,6 +1518,30 @@ +@@ -1403,6 +1519,30 @@ if (!ws_active()) return (int16_t)x; int32_t cx = ws_disp_w() / 2; /* screen centre (=160 @ 320) */ return ws_scale_about((int16_t)x, cx); @@ -242,7 +250,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Backdrop PRELOAD predicate + value substitution ([widescreen.cull] -@@ -1616,17 +1755,10 @@ +@@ -1616,17 +1756,10 @@ ws_nw_sync_target(); } @@ -263,7 +271,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e uint32_t now = (uint32_t)s_frame_count; uint32_t idx = (key >> 2) & (WS_TAG_BUCKETS - 1); uint32_t victim = idx; -@@ -1638,8 +1770,169 @@ +@@ -1638,8 +1771,169 @@ } ws_tags[victim].key = key; ws_tags[victim].stamp = now; @@ -434,7 +442,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Look up the executing GP0 command's prim in the tag table. The command's -@@ -1649,11 +1942,12 @@ +@@ -1649,11 +1943,12 @@ if (!ws_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) return 0; uint32_t now = (uint32_t)s_frame_count; for (int variant = 0; variant < 2; variant++) { @@ -449,7 +457,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e *out_ax = t->anchor_x; return 1; } -@@ -1786,11 +2080,12 @@ +@@ -1786,11 +2081,12 @@ if (!ws_engaged() || gp0_cmd_source_addr == 0xFFFFFFFFu) return 0; uint32_t now = (uint32_t)s_frame_count; for (int variant = 0; variant < 2; variant++) { @@ -464,7 +472,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } } return 0; -@@ -1820,6 +2115,12 @@ +@@ -1820,6 +2116,12 @@ GpuDisplayInfo di; gpu_get_display_info(&di); return di.height ? (int32_t)di.height : 240; @@ -477,7 +485,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Full-screen fades and environmental filters are authored as 320x240 TILEs. -@@ -1842,9 +2143,10 @@ +@@ -1842,9 +2144,10 @@ * the middle third to centre. A composite (counter box + digits) sits inside * one zone, so its pieces share a pivot and stay aligned. (Full-2D menu * screens never reach here — they get zero squash + 4:3 pillarbox instead.) */ @@ -489,7 +497,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e if (3 * cx < 2 * W) return 0; if (3 * cx > 4 * W) return W; return W / 2; -@@ -1861,7 +2163,7 @@ +@@ -1861,7 +2164,7 @@ if (!ws_auto_ui_squash || !ws_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) return 0; @@ -498,7 +506,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { if (ws_ui_prepass[i].src_addr != src) continue; if (out_anchor) *out_anchor = ws_ui_prepass[i].group.anchor; -@@ -1946,20 +2248,102 @@ +@@ -1946,20 +2249,104 @@ return 1; } @@ -587,6 +595,8 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e return (int)ws_scale_len(w); } + int32_t widget_w = w, widget_h = w; ++ const int held_w = ws_hud_held_rect(x0, w); ++ if (held_w) return held_w; + if (ws_hud_widget_rect(x0, y0, &widget_w, &widget_h)) { + *out_h = (int)widget_h; + return (int)widget_w; @@ -604,7 +614,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e *x0 = ws_scale_about(*x0, ws_hud_pivot(*x0, w)); return (int)ws_scale_len(w); } -@@ -2024,6 +2408,10 @@ +@@ -2024,6 +2411,10 @@ * the game's dedicated HUD packet arena reaches here, never world polygons. */ static void ws_nw_hud_shift_vertices(int32_t *vx, int count) { if (count <= 0) return; @@ -615,7 +625,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Sprite-tag titles: polygon/line prims are the GTE world and the tagged * character billboards, never HUD — only the rect-family sites (which * call ws_nw_hud_shift directly, with the untagged filter) re-anchor. */ -@@ -2103,6 +2491,7 @@ +@@ -2103,6 +2494,7 @@ * CPU-visible transfer remains ordered because GP0 accepts no next command * until this payload is complete. Maximum PS1 transfer = full VRAM (1 MiB). */ static uint16_t vram_write_pixels[1024 * 512]; @@ -623,7 +633,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Depth24 CPU→VRAM upload span (halfwords, exclusive end). See * gpu_depth24_rgb_limit — declared early so gpu_reset_state can clear it. */ -@@ -2110,6 +2499,58 @@ +@@ -2110,6 +2502,58 @@ static int s_d24_present_hold = 0; /* vblanks to skip Swap after GP1(07h) */ static uint32_t s_d24_prev_disp_h = 0; /* last GP1(07h) band height */ static void depth24_note_upload(uint32_t x, uint32_t w); @@ -682,7 +692,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e static void gp0_commit_cpu_to_vram(void) { for (uint32_t row = 0; row < vram_write_h; row++) -@@ -2120,6 +2561,11 @@ +@@ -2120,6 +2564,11 @@ gr_vram_transfer_in(vram_write_x, vram_write_y, vram_write_w, vram_write_h, vram_write_pixels); depth24_note_upload(vram_write_x, vram_write_w); @@ -694,7 +704,35 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gp0_state = GP0_IDLE; vram_write_remaining = 0; text_xlate_vram_upload(vram_write_x, vram_write_y, -@@ -2221,11 +2667,186 @@ +@@ -2137,6 +2586,27 @@ + /* Texture page / draw mode (set by GP0(E1h), reflected in GPUSTAT bits 0-10) */ + static uint32_t texpage_x; /* bits 0-3: texture page X base (N*64) */ + static uint32_t texpage_y; /* bit 4: texture page Y base (0 or 256) */ ++ ++static int ws_hud_held_page_x = -1, ws_hud_held_page_y = -1; ++static int ws_hud_held_seen; /* a sprite of the held weapon was drawn in this list */ ++static int32_t ws_hud_held_pivot; /* and squashed about this column, as the rest of the weapon will be */ ++void gpu_ws_set_hud_held_page(int page_x, int page_y) { ++ ws_hud_held_page_x = page_x; ++ ws_hud_held_page_y = page_y; ++} ++/* The width a sprite of the held weapon is drawn with, its left edge moved, or 0 when the sprite is no weapon's. */ ++static int ws_hud_held_rect(int32_t *x, int w) { ++ if (!ws_hud.in_hud_layer || w <= 0 || ws_hud_held_page_x < 0 || texpage_x != (uint32_t)ws_hud_held_page_x || ++ texpage_y != (uint32_t)ws_hud_held_page_y) ++ return 0; ++ if (!ws_hud_held_seen) { ++ ws_hud_held_pivot = psx_ws_hud_held_pivot(*x, w, ws_disp_w()); ++ ws_hud_held_seen = 1; ++ } ++ *x = ws_scale_about(*x, ws_hud_held_pivot); ++ w = (int)ws_scale_len(w); ++ return w > 0 ? w : 1; ++} + static uint32_t semi_transparency; /* bits 5-6 */ + static uint32_t texpage_colors; /* bits 7-8: 4bit/8bit/15bit */ + static uint32_t dither_enabled; /* bit 9 */ +@@ -2221,11 +2691,186 @@ * when native-wide is active and that buffer is a known display buffer; else * disable mirroring for this draw. Called when the draw env changes. */ static void ws_nw_sync_target(void) { @@ -883,10 +921,11 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } static void ws_clear_all_reveal_margins(void) { -@@ -2239,18 +2860,23 @@ +@@ -2239,18 +2884,24 @@ * stage art. A stale reveal tile is safer than deleting submitted content. */ void gpu_ws_begin_linked_list(void) { gp0_ot_rank = 0xFFFFu; ++ ws_hud_held_seen = 0; + psx_ws_hud_widgets_list(&ws_hud, ws_hud.count && ws_active() ? psx_read_word(ws_hud_table_pointer) : 0u); } @@ -907,7 +946,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } -@@ -2362,6 +2988,16 @@ +@@ -2362,6 +3013,16 @@ s_d24_upload_x1 = 0; s_d24_present_hold = 0; s_d24_prev_disp_h = 0; @@ -924,7 +963,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } void gpu_init(void) { -@@ -2538,6 +3174,9 @@ +@@ -2538,6 +3199,9 @@ } g_doff_min_this = 0x7fffffff; g_doff_max_this = -0x7fffffff; g_doff_cnt_this = 0; gpustat_poll_count = 0; @@ -934,7 +973,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Trusted package-selected plugins run on guest VBlank, independent of * host presentation, pacing, turbo, or skipped frames. */ mod_runtime_on_vblank(); -@@ -2757,73 +3396,522 @@ +@@ -2757,73 +3421,522 @@ *y = sign_extend((word >> 16) & 0x7FFu, 11); } @@ -1513,7 +1552,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Write a single pixel to VRAM with draw area clipping and mask bit handling */ -@@ -3009,8 +4097,7 @@ +@@ -3009,8 +4122,7 @@ } if (draw_area_out_bbox(vx, vy, 3)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); @@ -1523,7 +1562,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); } -@@ -3046,6 +4133,7 @@ +@@ -3046,6 +4158,7 @@ gr_draw_flat_rect(x, y, w, h, color); return; } @@ -1531,7 +1570,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (no-op else) */ ws_nw_hud_shift_vertices(vx, 4); for (int i = 0; i < 4; i++) { -@@ -3054,18 +4142,17 @@ +@@ -3054,18 +4167,17 @@ } if (draw_area_out_bbox(vx, vy, 4)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); @@ -1557,7 +1596,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); } } -@@ -3088,8 +4175,7 @@ +@@ -3088,8 +4200,7 @@ } if (draw_area_out_bbox(vx, vy, 3)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); @@ -1567,7 +1606,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], vx[2], vy[2], c[2]); -@@ -3131,20 +4217,19 @@ +@@ -3131,20 +4242,19 @@ } } gr_set_semi_transparency(semi_trans, (int)semi_transparency); @@ -1595,7 +1634,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_gouraud_triangle(vx[2], vy[2], c[2], vx[1], vy[1], c[1], vx[3], vy[3], c[3]); -@@ -3215,9 +4300,8 @@ +@@ -3215,9 +4325,8 @@ if (draw_area_out_bbox(vx, vy, 3)) return; setup_textured_draw(color24, semi_trans, raw_texture); @@ -1607,7 +1646,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], vx[1], vy[1], u[1], v[1], vx[2], vy[2], u[2], v[2], -@@ -3252,11 +4336,16 @@ +@@ -3252,11 +4361,16 @@ /* Widescreen: tagged billboard quads carry CPU-computed pixel offsets the * GTE squash never saw — re-squash every X around the prim's anchor. */ @@ -1626,7 +1665,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e ws_auto_ui_transform_quad(vx, vy); ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop image stretch (no-op else) */ ws_nw_hud_shift_vertices(vx, 4); -@@ -3282,6 +4371,13 @@ +@@ -3282,6 +4396,13 @@ int top_v = vy[0] < vy[2] ? v[0] : v[2]; int bot_v = vy[0] < vy[2] ? v[2] : v[0]; if (w > 0 && h > 0) { @@ -1640,7 +1679,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e if (right_u - left_u == w && bot_v - top_v == h) { gr_draw_textured_rect(x, y, w, h, left_u, top_v, clut_x, clut_y, tpage); -@@ -3294,23 +4390,25 @@ +@@ -3294,23 +4415,25 @@ } } @@ -1676,7 +1715,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], vx[1], vy[1], u[1], v[1], vx[3], vy[3], u[3], v[3], -@@ -3351,9 +4449,8 @@ +@@ -3351,9 +4474,8 @@ if (draw_area_out_bbox(vx, vy, 3)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); @@ -1688,7 +1727,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], vx[1], vy[1], u[1], v[1], c[1], vx[2], vy[2], u[2], v[2], c[2], -@@ -3399,23 +4496,25 @@ +@@ -3399,23 +4521,25 @@ if (draw_area_out_bbox(vx, vy, 4)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); @@ -1724,7 +1763,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], vx[1], vy[1], u[1], v[1], c[1], vx[3], vy[3], u[3], v[3], c[3], -@@ -3477,6 +4576,7 @@ +@@ -3477,6 +4601,7 @@ if (w > 1023) w = 1023; if (h > 511) h = 511; ws_expand_fullscreen_rect(&x0, y0, &w, h); @@ -1732,7 +1771,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e x0 += ws_nw_hud_shift(x0, w); /* native-wide HUD corner re-anchor (no-op else) */ x0 += draw_offset_x; y0 += draw_offset_y; if (draw_area_out_rect(x0, y0, w, h)) return; -@@ -3504,35 +4604,76 @@ +@@ -3504,35 +4629,79 @@ /* Widescreen: tagged sprite parts squash around their projected anchor; * untagged SPRTs are screen-space 2D (HUD/menus) and squash around the * display centre. Texels keep full coverage via the scaled-rect path. */ @@ -1772,7 +1811,10 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e int corrected_w = w; - if (ws_auto_ui_transform_rect(&x0, y0, &corrected_w, h)) + int32_t widget_w = w, widget_h = h; -+ if (ws_hud_widget_rect(&x0, &y0, &widget_w, &widget_h)) { ++ const int held_w = ws_hud_held_rect(&x0, w); ++ if (held_w) { ++ ws_w = held_w; ++ } else if (ws_hud_widget_rect(&x0, &y0, &widget_w, &widget_h)) { + ws_w = (int)widget_w; + ws_h = (int)widget_h; + } else if (ws_auto_ui_transform_rect(&x0, y0, &corrected_w, h)) @@ -1821,7 +1863,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Execute 1x1 dot (GP0 0x68-0x6B) */ -@@ -3541,6 +4682,7 @@ +@@ -3541,6 +4710,7 @@ uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu); int32_t x, y; parse_vertex(gp0_cmd_buf[1], &x, &y); @@ -1829,7 +1871,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e x += ws_nw_hud_shift(x, 1); x += draw_offset_x; y += draw_offset_y; if (draw_area_out_point(x, y)) return; -@@ -3555,13 +4697,12 @@ +@@ -3555,13 +4725,12 @@ int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); @@ -1847,7 +1889,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e int u0 = gp0_cmd_buf[2] & 0xFF; int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF; uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16); -@@ -3569,8 +4710,8 @@ +@@ -3569,8 +4738,8 @@ uint16_t clut_y = (clut >> 6) & 0x1FF; setup_textured_draw(color24, semi_trans, raw_texture); @@ -1858,7 +1900,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, 8, 8, u0, v0, clut_x, clut_y, current_texpage()); -@@ -3582,11 +4723,12 @@ +@@ -3582,11 +4751,12 @@ uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu); int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); @@ -1873,7 +1915,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } /* Execute 16x16 textured sprite (GP0 0x7C-0x7F) */ -@@ -3599,7 +4741,8 @@ +@@ -3599,7 +4769,8 @@ /* Never suppress MMX6 BG packets at a guessed finite-map boundary. The * classifier cannot distinguish an authored layer entering the reveal from * a stale ring slot; suppressing here caused the stage-start black flicker. */ @@ -1883,7 +1925,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e x0 += ws_nw_hud_shift(x0, 16); /* native-wide HUD corner re-anchor (no-op else) */ x0 += draw_offset_x; y0 += draw_offset_y; int u0 = gp0_cmd_buf[2] & 0xFF; -@@ -3608,13 +4751,11 @@ +@@ -3608,13 +4779,11 @@ uint16_t clut_x = (clut & 0x3F) * 16; uint16_t clut_y = (clut >> 6) & 0x1FF; @@ -1901,7 +1943,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage()); -@@ -3642,12 +4783,15 @@ +@@ -3642,12 +4811,15 @@ /* Fill ignores draw area, mask bits, and draw offset — writes directly to * VRAM. Routed through the renderer so it also fills the hi-res * supersampling mirror (no-op cost when supersampling is off). */ @@ -1921,7 +1963,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e gr_wide_clear((int)dst_x, (int)dst_y, (int)height, color16); } } -@@ -3682,6 +4826,11 @@ +@@ -3682,6 +4854,11 @@ gr_set_texture_window(texture_window_value); } @@ -1933,7 +1975,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e static void gp0_exec_draw_area_tl(void) { /* 0xE3 — Set Drawing Area Top-Left * Bits 0-9: X -@@ -3691,7 +4840,12 @@ +@@ -3691,7 +4868,12 @@ draw_area_top = (param >> 10) & 0x3FF; gr_set_draw_area((int)draw_area_left, (int)draw_area_top, (int)draw_area_right, (int)draw_area_bottom); @@ -1946,7 +1988,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e } static void gp0_exec_draw_area_br(void) { -@@ -3703,6 +4857,7 @@ +@@ -3703,6 +4885,7 @@ draw_area_bottom = (param >> 10) & 0x3FF; gr_set_draw_area((int)draw_area_left, (int)draw_area_top, (int)draw_area_right, (int)draw_area_bottom); @@ -1954,7 +1996,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */ } -@@ -3799,7 +4954,10 @@ +@@ -3799,7 +4982,10 @@ /* 0 means max dimension */ vram_write_w = (w == 0) ? 0x400 : (uint16_t)w; vram_write_h = (h == 0) ? 0x200 : (uint16_t)h; @@ -1966,7 +2008,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Record for debug */ if (a0_history_count < A0_HISTORY_CAP) { int slot = a0_history_count++; -@@ -3879,6 +5037,7 @@ +@@ -3879,6 +5065,7 @@ uint32_t h = (gp0_cmd_buf[2] >> 16) & 0x1FFu; vram_read_w = (w == 0) ? 0x400 : (uint16_t)w; vram_read_h = (h == 0) ? 0x200 : (uint16_t)h; @@ -1974,7 +2016,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Record for debug */ if (c0_history_count < C0_HISTORY_CAP) { -@@ -4047,7 +5206,7 @@ +@@ -4047,7 +5234,7 @@ item->group.x = min_x; item->group.width = width; item->group.anchor = 0; @@ -1983,7 +2025,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e item->ot_rank = rank; } -@@ -4397,6 +5556,10 @@ +@@ -4397,6 +5584,10 @@ extern void ws_bg_phase_note(uint32_t op); ws_bg_phase_note(opcode); /* native-wide 2D-backdrop stretch: background-phase latch */ @@ -1994,7 +2036,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e /* Draw-census: capture every drawing primitive's first vertex + camera. */ if (opcode >= 0x20 && opcode <= 0x7F) { int32_t cvx, cvy; -@@ -4499,7 +5662,8 @@ +@@ -4499,7 +5690,8 @@ int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); @@ -2004,7 +2046,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e x0 += draw_offset_x; y0 += draw_offset_y; int u0 = gp0_cmd_buf[2] & 0xFF; int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF; -@@ -4522,9 +5686,10 @@ +@@ -4522,9 +5714,10 @@ uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu); int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); @@ -2016,7 +2058,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e break; } case 0x7C: case 0x7D: case 0x7E: case 0x7F: -@@ -4549,7 +5714,10 @@ +@@ -4549,7 +5742,10 @@ int h = (gp0_cmd_buf[3] >> 16) & 0x1FF; if (w == 0) w = 0x400; if (h == 0) h = 0x200; @@ -2027,7 +2069,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e break; } -@@ -4901,7 +6069,11 @@ +@@ -4901,7 +6097,11 @@ hres1 = val & 3; vres = (val >> 2) & 1; video_mode = (val >> 3) & 1; @@ -2040,7 +2082,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e vertical_interlace = (val >> 5) & 1; /* GPUSTAT.13 holds the legacy constant 0 in progressive (see the vblank * field flip); clear it on the switch so a title that toggles interlace -@@ -5087,11 +6259,16 @@ +@@ -5087,11 +6287,16 @@ } void gpu_snapshot_write(uint8_t *p) { PstW w; uint32_t n = gpu_snapshot_bytes(); @@ -2058,7 +2100,7 @@ index 6d81477285f786cdd3658d14c424d4bfbab5d809..03a9bb50a3a0acf42c4593027554279e void gpu_cosim_dump(char *out, int cap) { if (!out || cap <= 0) return; char *p = out; -@@ -5136,10 +6313,14 @@ +@@ -5136,10 +6341,14 @@ if (len != gpu_snapshot_bytes()) return 0; pst_r_init(&r, p, len); if (!gpu_snap_parse(&r)) return 0; diff --git a/src/game/render/hud_widgets.cpp b/src/game/render/hud_widgets.cpp index 8f9c230..7454fa0 100644 --- a/src/game/render/hud_widgets.cpp +++ b/src/game/render/hud_widgets.cpp @@ -14,6 +14,13 @@ * panel and takes the panel's pivot. The name is text: the game prints it * afterwards as a list of its own, from a node on its stack. There are two * tables, one per frame buffer, and the word below names either. + * + * The weapon in the player's hands hangs on the same node. Its picture of the + * moment is at the top left of texture page 15, in one sprite for the baton + * and two for a gun. A gun stays in the middle of the screen. The baton rests + * against the right and bottom edges, which crop its picture, and swings in + * from the middle, so by the thirds rule it was a HUD box at rest, shrunk + * with the HUD size about the screen's edge, and a weapon in the swing. */ #include "gpu.h" @@ -27,11 +34,13 @@ constexpr uint32_t kTablePointer = 0x80071490u; /* gp + 0x344 */ constexpr int kHudLayers = 1; /* x0, y0, x1, y1, pivot x, pivot y: the weapon list, then the psionics list. Left out: the ammunition box from column 12, the psionic charge from row 170. */ constexpr int32_t kPanels[2][6] = {{16, 10, 128, 196, 0, 0}, {232, 10, 298, 170, 320, 0}}; +constexpr int kHeldPage[2] = {15, 0}; constexpr int kTextStrip = 128; /* a printed line is one picture this wide: NO of the pause menu begins at column 169 and reaches 296 */ PSX_MOD_CONSTRUCTOR(register_disruptor_hud_widgets) { gpu_ws_set_hud_widgets(kTablePointer, kHudLayers, kPanels[0], 2); gpu_ws_set_hud_text_strip(kTextStrip); + gpu_ws_set_hud_held_page(kHeldPage[0], kHeldPage[1]); } } // namespace diff --git a/tests/contracts/test_hud_widget_contract.py b/tests/contracts/test_hud_widget_contract.py index 1b8c594..de0ea73 100644 --- a/tests/contracts/test_hud_widget_contract.py +++ b/tests/contracts/test_hud_widget_contract.py @@ -99,27 +99,64 @@ def body(source: str, signature: str) -> str: "only an axis-aligned quad wholly inside a widget may be mapped, corner by corner", ) +held = body(gpu, "static int ws_hud_held_rect(int32_t *x, int w) {") +scale_header = read("psxrecomp/runtime/include/gpu_ws_hud_scale.h") +require( + " if (!ws_hud.in_hud_layer || w <= 0 || ws_hud_held_page_x < 0 || texpage_x != (uint32_t)ws_hud_held_page_x ||\n" + " texpage_y != (uint32_t)ws_hud_held_page_y)\n return 0;\n" in held + and " if (!ws_hud_held_seen) {\n ws_hud_held_pivot = psx_ws_hud_held_pivot(*x, w, ws_disp_w());\n" + " ws_hud_held_seen = 1;\n }\n" in held + and " *x = ws_scale_about(*x, ws_hud_held_pivot);\n w = (int)ws_scale_len(w);\n return w > 0 ? w : 1;" in held + and "ws_hud_user_scale" not in held and "ws_hud_scale_percent" not in held + and " gp0_ot_rank = 0xFFFFu;\n ws_hud_held_seen = 0;\n" in body(gpu, "void gpu_ws_begin_linked_list(void) {") + and gpu.count("ws_hud_held_seen = 0;") == 1 + and "static int ws_hud_held_page_x = -1, ws_hud_held_page_y = -1;" in gpu + and "void gpu_ws_set_hud_held_page(int page_x, int page_y);" in header, + "a sprite of the held weapon's texture page in a HUD layer keeps its size whatever the HUD size, and all of the " + "weapon in one list is squashed about the column its first sprite gives", +) +require( + " if (w <= 0 || screen_w <= 0 || w >= screen_w) return screen_w / 2;\n" in scale_header + and " mirrored = centre < screen_w;\n" in scale_header + and " start = 12 * (int64_t)w <= 5 * (int64_t)screen_w ? 4 * (int64_t)screen_w : 3 * (int64_t)screen_w;" in scale_header + and " gone = 3 * centre - start;\n if (gone <= 0) return screen_w / 2;\n" in scale_header + and " reach = 3 * (2 * (int64_t)screen_w - w) - start;\n if (gone > reach) gone = reach;\n" in scale_header + and " centre = ((int64_t)screen_w * (reach + gone) + reach) / (2 * reach);\n" in scale_header + and " return (int32_t)(mirrored ? screen_w - centre : centre);\n" in scale_header, + "the weapon's column is the centre in the middle third and slides to the edge the sprite comes to touch", +) +require( + "constexpr int kHeldPage[2] = {15, 0};" in module + and " gpu_ws_set_hud_held_page(kHeldPage[0], kHeldPage[1]);\n" in module + and module.count("gpu_ws_set_hud_held_page(") == 1, + "the game must say that it keeps the held weapon's picture at the top of texture page 15", +) fixed = body(gpu, "static int ws_sprt_fixed_transform(") textured_rect = body(gpu, "static void gp0_exec_textured_rect(void)") flat_quad = body(gpu, "static void gp0_exec_mono_quad(void)") textured_quad = body(gpu, "static void gp0_exec_textured_quad(void)") require( " int32_t widget_w = w, widget_h = w;\n" + " const int held_w = ws_hud_held_rect(x0, w);\n" + " if (held_w) return held_w;\n" " if (ws_hud_widget_rect(x0, y0, &widget_w, &widget_h)) {\n" " *out_h = (int)widget_h;\n" " return (int)widget_w;\n" " }\n" " int auto_w = w;" in fixed and fixed.index("ws_tagged_anchor(&ax)") < fixed.index("ws_hud_widget_rect("), - "a fixed-size sprite must try its widget after the tag and before every other HUD rule", + "a fixed-size sprite must be tried as the held weapon and then as a widget, after the tag and before every other HUD rule", ) require( " int32_t widget_w = w, widget_h = h;\n" - " if (ws_hud_widget_rect(&x0, &y0, &widget_w, &widget_h)) {\n" + " const int held_w = ws_hud_held_rect(&x0, w);\n" + " if (held_w) {\n" + " ws_w = held_w;\n" + " } else if (ws_hud_widget_rect(&x0, &y0, &widget_w, &widget_h)) {\n" " ws_w = (int)widget_w;\n" " ws_h = (int)widget_h;\n" " } else if (ws_auto_ui_transform_rect(&x0, y0, &corrected_w, h))" in textured_rect, - "a textured rectangle must try its widget before every other HUD rule", + "a textured rectangle must be tried as the held weapon and then as a widget, before every other HUD rule", ) require( flat_quad.count("ws_hud_widget_quad(vx, vy);") == 1 diff --git a/tests/unit/gpu_ws_hud_scale_test.cpp b/tests/unit/gpu_ws_hud_scale_test.cpp index 9f4dcb6..ca6b359 100644 --- a/tests/unit/gpu_ws_hud_scale_test.cpp +++ b/tests/unit/gpu_ws_hud_scale_test.cpp @@ -208,6 +208,47 @@ void test_a_line_of_text_is_placed_by_where_its_words_can_be() { "a call that is refused works nothing out of the rectangle first"); } +/* The baton of Disruptor: at rest against the right edge, then the four pictures of its swing. */ +constexpr std::array kBaton{{{216, 79, 104, 161}, {119, 206, 52, 34}, {119, 145, 64, 95}, {143, 92, 94, 148}, {176, 49, 128, 191}}}; + +void test_a_held_weapon_slides_to_the_edge_it_touches() { + for (const Rect &gun : {Rect{83, 183, 122, 57}, Rect{103, 179, 106, 61}, Rect{109, 173, 82, 67}, kWeapon}) + expect(psx_ws_hud_held_pivot(gun.x, gun.w, kWidth) == kWidth / 2, "a gun in the middle of the screen is squashed about the centre"); + expect(psx_ws_hud_held_pivot(kBaton[0].x, kBaton[0].w, kWidth) == kWidth, "the baton at rest touches the right edge and is squashed about it"); + for (std::int32_t sway = 1; sway <= 40; ++sway) + expect(psx_ws_hud_held_pivot(kBaton[0].x + sway, kBaton[0].w, kWidth) == kWidth, "and when it sways past the edge"); + expect(psx_ws_hud_held_pivot(kBaton[1].x, kBaton[1].w, kWidth) == 160 && psx_ws_hud_held_pivot(kBaton[2].x, kBaton[2].w, kWidth) == 160 && + psx_ws_hud_held_pivot(kBaton[3].x, kBaton[3].w, kWidth) == 160, + "the first three pictures of the swing are in the middle third"); + expect(psx_ws_hud_held_pivot(kBaton[4].x, kBaton[4].w, kWidth) == 260, "the fourth is five eighths of the way to touching: 260"); + for (const Aspect &aspect : kAspects) { + for (const std::int32_t w : {36, 52, 104, 128, 133, 134, 200, 220, 240}) { + std::int32_t last_left = -100000, last_pivot = 0; + for (std::int32_t x = -w; x <= kWidth; ++x) { + const std::int32_t pivot = psx_ws_hud_held_pivot(x, w, kWidth); + const std::int32_t left = pivot + (x - pivot) * aspect.numerator / aspect.denominator; + expect(pivot >= 0 && pivot <= kWidth && pivot >= last_pivot, "the column stays on the screen and never goes back as the sprite goes right"); + expect(left >= last_left, "the sprite never goes back as it goes right"); + expect(last_left == -100000 || left - last_left <= 4, "and never jumps, whatever its width up to three quarters of the screen"); + expect(psx_ws_hud_held_pivot(kWidth - x - w, w, kWidth) == kWidth - pivot, "the left side is the right side's mirror"); + if (x + w >= kWidth) expect(pivot == kWidth, "a sprite the right edge crops is squashed about that edge"); + last_left = left; + last_pivot = pivot; + } + } + } + expect(psx_ws_hud_held_pivot(100, 220, kWidth) == kWidth && psx_ws_hud_held_pivot(50, 220, kWidth) == 160 && + psx_ws_hud_held_pivot(60, 220, kWidth) == 192 && psx_ws_hud_held_pivot(40, 220, kWidth) == 128, + "a sprite too wide for an outer third slides all the way from the centre to the edge it comes to touch"); + expect(psx_ws_hud_held_pivot(93, 133, kWidth) == 160 && psx_ws_hud_held_pivot(93, 134, kWidth) == 160 && + psx_ws_hud_held_pivot(120, 133, kWidth) == 160 && psx_ws_hud_held_pivot(120, 134, kWidth) > 160, + "five twelfths of the screen is where the slide's start moves from the third to the centre"); + expect(psx_ws_hud_held_pivot(0, kWidth, kWidth) == 160 && psx_ws_hud_held_pivot(30, kWidth + 40, kWidth) == 160, + "a sprite as wide as the screen has no edge to come to"); + expect(psx_ws_hud_held_pivot(200, 0, kWidth) == 160 && psx_ws_hud_held_pivot(200, -4, kWidth) == 160 && psx_ws_hud_held_pivot(200, 50, 0) == 0, + "an empty sprite or screen gives the centre"); +} + } // namespace int main() { @@ -217,6 +258,7 @@ int main() { test_widgets_shrink_toward_their_own_edge(); test_neighbours_keep_touching(); test_exact_values(); + test_a_held_weapon_slides_to_the_edge_it_touches(); if (g_failures) return 1; std::cout << "GPU widescreen HUD scale tests passed\n"; return 0;