diff --git a/docs/WIDESCREEN.md b/docs/WIDESCREEN.md index 741e6f66..9613631d 100644 --- a/docs/WIDESCREEN.md +++ b/docs/WIDESCREEN.md @@ -140,6 +140,10 @@ auto_ui_anchor = "edges" # "edges" (default): each run pins to # its left/centre/right third. # "in_place": each run squashes about # its own centre. +auto_ui_size = "proportional" # "original" (default): the HUD keeps + # the display height's scale. + # "proportional": beyond 16:9 it shrinks + # by sqrt((16:9) / aspect). clear_reveal = true # clear synthetic native-wide side margins # at opted-in scene/map boundaries (default false). nw_left_hud_packet_lo = "0x000E3400" # optional targeted left-HUD packet range @@ -204,6 +208,16 @@ its own centre instead, which corrects its proportions and leaves it where the stretched 4:3 layout places it. `gpu_state` reports the mode as `ws.auto_ui.in_place`. +The HUD is proportion-corrected horizontally but keeps the display height's +scale, so on a very wide window it reads large against the much wider view. +`auto_ui_size = "proportional"` leaves it unchanged up to 16:9 and shrinks it +by sqrt((16:9) / aspect) beyond (0.87 at 21:9, 0.71 at 32:9), on both axes: +each run about its horizontal anchor and its vertical anchor (its top edge in +the upper half of the display, its bottom edge in the lower half), scaling +both edges of rectangles and sprites so neighbouring parts stay butted. A mod +may switch it per session with `psx_mod_set_widescreen_hud_size()`. +Projection widescreen only (native-wide does not squash the HUD). + **Changing `sprite_tag_funcs` requires a game regen** (the tag callback is emitted into the generated C). `widescreen.cull.keep` is consumed by both the native recompiler and the dirty-RAM interpreter: regenerate main/overlay native diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 438cc04b..072c2d69 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -1673,6 +1673,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { bool ws_hud_sprt_squash = false; bool ws_auto_ui_squash = false; bool ws_auto_ui_in_place = false; + bool ws_auto_ui_proportional = false; bool ws_full_2d = false; bool ws_gte_game_mode = false; bool ws_precise_nclip = false; @@ -1836,6 +1837,15 @@ GameConfig load_game_config(const fs::path& config_path_in) { "{}: [widescreen] auto_ui_anchor must be \"edges\" or " "\"in_place\", got \"{}\"", config_path.string(), anchor)); } + if (ws.contains("auto_ui_size")) { + const auto size = toml::find(ws, "auto_ui_size"); + if (size == "proportional") + ws_auto_ui_proportional = true; + else if (size != "original") + throw std::runtime_error(fmt::format( + "{}: [widescreen] auto_ui_size must be \"original\" or " + "\"proportional\", got \"{}\"", config_path.string(), size)); + } if (ws.contains("full_2d")) ws_full_2d = toml::find(ws, "full_2d"); if (ws.contains("gte_game_mode")) @@ -2480,6 +2490,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_hud_sprt_squash*/ ws_hud_sprt_squash, /*ws_auto_ui_squash*/ ws_auto_ui_squash, /*ws_auto_ui_in_place*/ ws_auto_ui_in_place, + /*ws_auto_ui_proportional*/ ws_auto_ui_proportional, /*data_shard_funcs*/ data_shard_funcs, /*mod_function_entry_funcs*/ mod_function_entry_funcs, /*mod_instruction_sites*/ mod_instruction_sites, diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 8d20f96e..6ae59030 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -963,6 +963,11 @@ struct GameConfig { // For HUD widgets that combine flat quads with GTE-projected parts the // correction cannot move (Spider-Man's compass ring and 3D arrow). bool ws_auto_ui_in_place = false; + // auto_ui_size = "proportional": beyond 16:9 the auto-UI HUD shrinks by + // sqrt((16:9) / aspect) about each widget's anchor, so a wide window + // does not show a 4:3-height HUD across a much wider view. "original" + // (default) keeps the HUD at the display height's scale. + bool ws_auto_ui_proportional = false; // [data_shards] funcs: functions that get the memoized pure-function // replay entry/return hooks (psx_datashard_enter/psx_datashard_ret). diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 43f99c2c..23750a13 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -212,6 +212,9 @@ void gpu_ws_set_auto_ui_squash(int on); /* [widescreen] auto_ui_anchor = "in_place": each UI run squashes about its own * centre rather than an edge/centre third (default "edges"). */ void gpu_ws_set_auto_ui_in_place(int on); +/* [widescreen] auto_ui_size: 1 = proportional (the auto-UI HUD shrinks by + * sqrt((16:9) / aspect) beyond 16:9), 0 = original. */ +void gpu_ws_set_auto_ui_proportional(int on); /* [widescreen.bg2d] Capcom 2D background tile-loop widen — hooked at the renderer's * column-count / start-tile-col / start-screen-x instructions. Identity at 4:3 * and in the engine's 512 hi-res mode. */ diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index a7e0bc61..97590729 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -393,6 +393,15 @@ int psx_mod_set_fixed_display_aspect(uint32_t numerator, */ int psx_mod_set_adaptive_display_aspect(uint32_t max_numerator, uint32_t max_denominator); +/* + * HUD size for the auto-UI widescreen HUD ([widescreen] auto_ui_squash): + * 0 keeps it at the display height's scale ("original"), 1 makes it + * proportional (unchanged up to 16:9, shrinking by sqrt((16:9) / aspect) + * beyond, about each widget's anchors). Overrides game.toml + * [widescreen] auto_ui_size for the session; every session start restores the + * title's setting before activation. + */ +int psx_mod_set_widescreen_hud_size(int proportional); /* * Set the wall-clock cadence of simulated guest VBlanks. A value of zero * removes frontend pacing; 60 and higher request that many native guest diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 2887699d..424bf70d 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -46,6 +46,8 @@ #include "ws_screen_mask.h" #include "ws_radial_screen_mask.h" #include +#include +#include #include #include #include @@ -140,6 +142,10 @@ typedef struct { /* A triangle or a rotated quad: UI only as part of a widget it lies * inside (ws_ui_prepass_admit_enclosed), never on its own. */ uint8_t enclosed_only; + /* Vertical anchor of the item's run (auto_ui_size proportional): the + * run's top edge in the upper half of the display, its bottom edge in + * the lower half, so a shrunken widget keeps hugging its screen edge. */ + int32_t anchor_y; WsPrepassPacketGuard packet_guard; } WsUiPrepassItem; static WsUiPrepassItem ws_ui_prepass[WS_UI_PREPASS_MAX]; @@ -245,6 +251,11 @@ void gpu_ws_set_full_2d(int on) { ws_full_2d = on ? 1 : 0; } void gpu_ws_set_auto_ui_in_place(int on) { ws_auto_ui_in_place = on ? 1 : 0; } +/* [widescreen] auto_ui_size (gpu.c ws_auto_ui_size). */ +static int ws_auto_ui_proportional = 0; +void gpu_ws_set_auto_ui_proportional(int on) { + ws_auto_ui_proportional = on ? 1 : 0; +} void gpu_ws_set_auto_ui_squash(int on) { ws_auto_ui_squash = on ? 1 : 0; @@ -2841,10 +2852,10 @@ static void ws_ui_prepass_invalidate_stale(void) { ws_ui_reject.stale++; } -static int ws_auto_ui_anchor(int32_t *out_anchor) { +static const WsUiPrepassItem *ws_auto_ui_item(void) { if (!ws_auto_ui_squash || !ws_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) - return 0; + return NULL; uint32_t src = GPU_RAM_KEY(gp0_cmd_source_addr); for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { if (ws_ui_prepass[i].src_addr != src) continue; @@ -2852,13 +2863,38 @@ static int ws_auto_ui_anchor(int32_t *out_anchor) { gp0_cmd_buf, (uint32_t)gp0_words_needed)) { ws_ui_prepass_invalidate_stale(); - return 0; + return NULL; } - if (out_anchor) *out_anchor = ws_ui_prepass[i].group.anchor; ws_auto_ui_candidate_count++; - return 1; + return &ws_ui_prepass[i]; } - return 0; + return NULL; +} + +static int ws_auto_ui_anchor(int32_t *out_anchor) { + const WsUiPrepassItem *it = ws_auto_ui_item(); + if (!it) return 0; + if (out_anchor) *out_anchor = it->group.anchor; + return 1; +} + +/* auto_ui_size = "proportional": the HUD keeps its authored size up to 16:9 + * and shrinks by sqrt((16:9) / aspect) beyond it (0.87 at 21:9, 0.71 at + * 32:9), on both axes about the widget's anchors. The squash factor + * xnum/xden is (4:3) / aspect, so (16:9) / aspect = (4/3) * xnum/xden. + * Projection widescreen only: native-wide does not squash the HUD. */ +static double ws_auto_ui_size(void) { + if (!ws_auto_ui_proportional || ws_mode == 2 || ws_xden <= 0) return 1.0; + const double r = (4.0 / 3.0) * (double)ws_xnum / (double)ws_xden; + return r >= 1.0 ? 1.0 : sqrt(r); +} +static int32_t ws_ui_x(int32_t x, int32_t ax, double s) { + if (s >= 1.0) return ws_scale_about(x, ax); + return ax + (int32_t)lround((double)(x - ax) * ws_xnum / ws_xden * s); +} +static int32_t ws_ui_y(int32_t y, int32_t ay, double s) { + if (s >= 1.0) return y; + return ay + (int32_t)lround((double)(y - ay) * s); } static int ws_shared_ui_packet(void) { @@ -2924,21 +2960,24 @@ static int ws_axis_aligned_quad(const int32_t vx[4], const int32_t vy[4]) { /* A widget part that is not an axis-aligned rectangle (a triangle, a rotated * quad): squashed with its widget when the prepass admitted it as lying * inside that widget (ws_ui_prepass_finish). */ -static int ws_auto_ui_transform_part(int32_t *vx, int n) { +static int ws_auto_ui_transform_part(int32_t *vx, int32_t *vy, int n) { if (ws_projection_hud_transform(vx, n)) return 1; if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) return 0; - int32_t anchor; - if (!ws_auto_ui_anchor(&anchor)) return 0; - for (int i = 0; i < n; i++) - vx[i] = ws_scale_about(vx[i], anchor) + ws_shared_ui_delta(); + const WsUiPrepassItem *it = ws_auto_ui_item(); + if (!it) return 0; + const double s = ws_auto_ui_size(); + for (int i = 0; i < n; i++) { + vx[i] = ws_ui_x(vx[i], it->group.anchor, s) + ws_shared_ui_delta(); + vy[i] = ws_ui_y(vy[i], it->anchor_y, s); + } ws_auto_ui_transform_count++; return 1; } -static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { +static int ws_auto_ui_transform_quad(int32_t vx[4], int32_t vy[4]) { if (ws_projection_hud_transform(vx, 4)) return 1; if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) return 0; - if (!ws_axis_aligned_quad(vx, vy)) return ws_auto_ui_transform_part(vx, 4); + if (!ws_axis_aligned_quad(vx, vy)) return ws_auto_ui_transform_part(vx, vy, 4); int32_t min_x = vx[0], max_x = vx[0], min_y = vy[0], max_y = vy[0]; for (int i = 1; i < 4; i++) { @@ -2953,16 +2992,19 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) { (width > W / 2 && height > H / 4)) return 0; - int32_t anchor; - if (!ws_auto_ui_anchor(&anchor)) return 0; - for (int i = 0; i < 4; i++) - vx[i] = ws_scale_about(vx[i], anchor) + ws_shared_ui_delta(); + const WsUiPrepassItem *it = ws_auto_ui_item(); + if (!it) return 0; + const double s = ws_auto_ui_size(); + for (int i = 0; i < 4; i++) { + vx[i] = ws_ui_x(vx[i], it->group.anchor, s) + ws_shared_ui_delta(); + vy[i] = ws_ui_y(vy[i], it->anchor_y, s); + } ws_auto_ui_transform_count++; return 1; } -static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) { - if (!x || !w || *w <= 0) return 0; +static int ws_auto_ui_transform_rect(int32_t *x, int32_t *y, int *w, int *h) { + if (!x || !y || !w || !h || *w <= 0) return 0; int32_t explicit_anchor; if (ws_projection_hud_anchor(&explicit_anchor)) { *x = ws_scale_about(*x, explicit_anchor); @@ -2972,13 +3014,26 @@ static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) { if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged()) return 0; int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h(); - if ((*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) || - (*w > W / 2 && h > H / 4)) + if ((*x <= X && *x + *w >= X + W && *y <= 0 && *y + *h >= H) || + (*w > W / 2 && *h > H / 4)) return 0; - int32_t anchor; - if (!ws_auto_ui_anchor(&anchor)) return 0; - *x = ws_scale_about(*x, anchor) + ws_shared_ui_delta(); - *w = (int)ws_scale_len(*w); + const WsUiPrepassItem *it = ws_auto_ui_item(); + if (!it) return 0; + const double s = ws_auto_ui_size(); + if (s >= 1.0) { + *x = ws_scale_about(*x, it->group.anchor) + ws_shared_ui_delta(); + *w = (int)ws_scale_len(*w); + } else { + /* Scale both edges so neighbouring parts of a widget stay butted. */ + const int32_t x0 = ws_ui_x(*x, it->group.anchor, s); + const int32_t x1 = ws_ui_x(*x + *w, it->group.anchor, s); + const int32_t y0 = ws_ui_y(*y, it->anchor_y, s); + const int32_t y1 = ws_ui_y(*y + *h, it->anchor_y, s); + *x = x0 + ws_shared_ui_delta(); + *w = x1 - x0 < 1 ? 1 : (int)(x1 - x0); + *y = y0; + *h = y1 - y0 < 1 ? 1 : (int)(y1 - y0); + } ws_auto_ui_transform_count++; return 1; } @@ -2986,7 +3041,7 @@ static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) { /* Shared transform for fixed-size textured sprites (8x8 / 16x16 / 1x1 dot): * squash *x0 in place (around the tagged anchor, else the HUD pivot) and * return the squashed draw width, or 0 = no change. */ -static int ws_sprt_fixed_transform(int32_t *x0, int32_t y0, int w) { +static int ws_sprt_fixed_transform(int32_t *x0, int32_t *y0, int w, int *h) { if (!ws_active()) return 0; int32_t ax; if (ws_tagged_anchor(&ax)) { @@ -2994,7 +3049,7 @@ static int ws_sprt_fixed_transform(int32_t *x0, int32_t y0, int w) { return (int)ws_scale_len(w); } int auto_w = w; - if (ws_auto_ui_transform_rect(x0, y0, &auto_w, w)) + if (ws_auto_ui_transform_rect(x0, y0, &auto_w, h)) return auto_w; if (ws_hud_sprt) { *x0 = ws_scale_about(*x0, ws_hud_pivot(*x0, w)); @@ -4715,7 +4770,7 @@ static void gp0_exec_mono_tri(void) { parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); } if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; - ws_auto_ui_transform_part(vx, 3); + ws_auto_ui_transform_part(vx, vy, 3); ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -4856,7 +4911,7 @@ static void gp0_exec_shaded_tri(void) { parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; - ws_auto_ui_transform_part(vx, 3); + ws_auto_ui_transform_part(vx, vy, 3); ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -5023,7 +5078,7 @@ static void gp0_exec_textured_tri(void) { uint16_t tpage = tpage_word & 0x1FF; set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; - ws_auto_ui_transform_part(vx, 3); + ws_auto_ui_transform_part(vx, vy, 3); ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -5171,7 +5226,7 @@ static void gp0_exec_shaded_textured_tri(void) { uint16_t tpage = tpage_word & 0x1FF; set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; - ws_auto_ui_transform_part(vx, 3); + ws_auto_ui_transform_part(vx, vy, 3); ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -5350,9 +5405,11 @@ static void gp0_exec_mono_rect(void) { * behind in open screen. Untouched when the prepass did not admit this * primitive, and rects never carry GTE output. */ if (ws_active() && w > 0) { - int corrected_w = w; - if (ws_auto_ui_transform_rect(&x0, y0, &corrected_w, h)) + int corrected_w = w, corrected_h = h; + if (ws_auto_ui_transform_rect(&x0, &y0, &corrected_w, &corrected_h)) { w = corrected_w; + h = corrected_h; + } } x0 += ws_nw_hud_shift(x0, w); /* native-wide HUD corner re-anchor (no-op else) */ x0 += draw_offset_x; y0 += draw_offset_y; @@ -5408,7 +5465,7 @@ static void gp0_exec_textured_rect(void) { /* 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. */ - int ws_w = 0; + int ws_w = 0, ws_h = h; if (ws_active() && w > 0) { int32_t ws_ax; if (ws_tagged_anchor(&ws_ax)) { @@ -5416,7 +5473,7 @@ static void gp0_exec_textured_rect(void) { ws_w = (int)ws_scale_len(w); } else { int corrected_w = w; - if (ws_auto_ui_transform_rect(&x0, y0, &corrected_w, h)) + if (ws_auto_ui_transform_rect(&x0, &y0, &corrected_w, &ws_h)) ws_w = corrected_w; else if (ws_hud_sprt) { x0 = ws_scale_about(x0, ws_hud_pivot(x0, w)); @@ -5427,13 +5484,11 @@ static void gp0_exec_textured_rect(void) { x0 += ws_nw_hud_shift(x0, w); /* native-wide HUD corner re-anchor (no-op else) */ x0 += draw_offset_x; y0 += draw_offset_y; - { - int dw = (ws_w && ws_w != w) ? ws_w : w; - if (draw_area_out_rect(x0, y0, dw, h)) return; - } + const int dw = (ws_w && ws_w != w) ? ws_w : w; + if (draw_area_out_rect(x0, y0, dw, ws_h)) return; setup_textured_draw(color24, semi_trans, raw_texture); - if (ws_w && ws_w != w) - gr_draw_textured_rect_scaled(x0, y0, ws_w, h, u0, v0, u0 + w, v0 + h, + if (dw != w || ws_h != h) + gr_draw_textured_rect_scaled(x0, y0, dw, ws_h, u0, v0, u0 + w, v0 + h, clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, w, h, u0, v0, clut_x, clut_y, current_texpage()); @@ -5446,7 +5501,7 @@ static void gp0_exec_mono_dot(void) { uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu); int32_t x, y; parse_vertex(gp0_cmd_buf[1], &x, &y); - ws_sprt_fixed_transform(&x, y, 1); /* auto_ui squash; no-op when unadmitted */ + { int dot_h = 1; ws_sprt_fixed_transform(&x, &y, 1, &dot_h); } /* auto_ui squash; no-op when unadmitted */ x += ws_nw_hud_shift(x, 1); x += draw_offset_x; y += draw_offset_y; if (draw_area_out_point(x, y)) return; @@ -5461,7 +5516,7 @@ static void gp0_exec_textured_dot(void) { int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); - (void)ws_sprt_fixed_transform(&x0, y0, 1); /* position only; 1px stays 1px */ + { int dot_h = 1; (void)ws_sprt_fixed_transform(&x0, &y0, 1, &dot_h); } /* position only; 1px stays 1px */ x0 += ws_nw_hud_shift(x0, 1); x0 += draw_offset_x; y0 += draw_offset_y; int u0 = gp0_cmd_buf[2] & 0xFF; @@ -5480,13 +5535,12 @@ static void gp0_exec_textured_8x8(void) { int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); - int ws_w = ws_sprt_fixed_transform(&x0, y0, 8); + int ws_h = 8; + int ws_w = ws_sprt_fixed_transform(&x0, &y0, 8, &ws_h); x0 += ws_nw_hud_shift(x0, 8); /* native-wide HUD corner re-anchor (no-op else) */ x0 += draw_offset_x; y0 += draw_offset_y; - { - int dw = (ws_w && ws_w != 8) ? ws_w : 8; - if (draw_area_out_rect(x0, y0, dw, 8)) return; - } + const int dw = (ws_w && ws_w != 8) ? ws_w : 8; + if (draw_area_out_rect(x0, y0, dw, ws_h)) return; 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); @@ -5494,8 +5548,8 @@ static void gp0_exec_textured_8x8(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; setup_textured_draw(color24, semi_trans, raw_texture); - if (ws_w && ws_w != 8) - gr_draw_textured_rect_scaled(x0, y0, ws_w, 8, u0, v0, u0 + 8, v0 + 8, + if (dw != 8 || ws_h != 8) + gr_draw_textured_rect_scaled(x0, y0, dw, ws_h, u0, v0, u0 + 8, v0 + 8, clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, 8, 8, u0, v0, clut_x, clut_y, current_texpage()); @@ -5507,14 +5561,15 @@ static void gp0_exec_mono_8x8(void) { uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu); int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); - int ws_w = ws_sprt_fixed_transform(&x0, y0, 8); + int ws_h = 8; + int ws_w = ws_sprt_fixed_transform(&x0, &y0, 8, &ws_h); x0 += ws_nw_hud_shift(x0, 8); x0 += draw_offset_x; y0 += draw_offset_y; { int dw = (ws_w && ws_w != 8) ? ws_w : 8; - if (draw_area_out_rect(x0, y0, dw, 8)) return; + if (draw_area_out_rect(x0, y0, dw, ws_h)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_flat_rect(x0, y0, dw, 8, color); + gr_draw_flat_rect(x0, y0, dw, ws_h, color); } } @@ -5533,7 +5588,8 @@ static void gp0_exec_textured_16x16(void) { /* A pending full-width OT can outlive a resize back to 4:3. Keep its * central tiles; the normal draw area clips the extra columns. Dropping * every host packet here would blank that transition frame. */ - int ws_w = host_tile ? 0 : ws_sprt_fixed_transform(&x0, y0, 16); + int ws_h = 16; + int ws_w = host_tile ? 0 : ws_sprt_fixed_transform(&x0, &y0, 16, &ws_h); if (!host_tile) x0 += ws_nw_hud_shift(x0, 16); x0 += draw_offset_x; y0 += draw_offset_y; int u0 = gp0_cmd_buf[2] & 0xFF; @@ -5542,10 +5598,8 @@ static void gp0_exec_textured_16x16(void) { uint16_t clut_x = (clut & 0x3F) * 16; uint16_t clut_y = (clut >> 6) & 0x1FF; - { - int dw = (ws_w && ws_w != 16) ? ws_w : 16; - if (draw_area_out_rect(x0, y0, dw, 16)) return; - } + const int dw = (ws_w && ws_w != 16) ? ws_w : 16; + if (draw_area_out_rect(x0, y0, dw, ws_h)) return; uint16_t bank = host_tile && (psx_read_word(host_packet + 28u) & ~GPU_WS_BG2D_MIRROR_X) == GPU_WS_BG2D_BANK_PACKET_MAGIC ? (uint16_t)(psx_read_word(host_packet + 16u) >> 16) : 0; @@ -5555,8 +5609,8 @@ static void gp0_exec_textured_16x16(void) { if (host_tile && (psx_read_word(host_packet + 28u) & GPU_WS_BG2D_MIRROR_X)) gr_draw_textured_rect_scaled(x0, y0, 16, 16, u0 + 15, v0, u0 - 1, v0 + 16, clut_x, clut_y, current_texpage()); - else if (ws_w && ws_w != 16) - gr_draw_textured_rect_scaled(x0, y0, ws_w, 16, u0, v0, u0 + 16, v0 + 16, + else if (dw != 16 || ws_h != 16) + gr_draw_textured_rect_scaled(x0, y0, dw, ws_h, u0, v0, u0 + 16, v0 + 16, clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage()); @@ -6335,6 +6389,32 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) { ws_ui_prepass[i].group.anchor = group_origin + groups[i].anchor; ws_ui_prepass[i].group.root = groups[i].root; } + /* Vertical anchor per run (auto_ui_size): its top edge in the upper half + * of the display, its bottom edge in the lower half. */ + { + static int32_t run_y0[WS_UI_PREPASS_MAX], run_y1[WS_UI_PREPASS_MAX]; + const int32_t half = ws_disp_h() / 2; + for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { + run_y0[i] = INT32_MAX; + run_y1[i] = INT32_MIN; + } + for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { + const uint32_t r = (uint32_t)groups[i].root; + if (r >= ws_ui_prepass_count) continue; + if (groups[i].y < run_y0[r]) run_y0[r] = groups[i].y; + if (groups[i].y + groups[i].height > run_y1[r]) + run_y1[r] = groups[i].y + groups[i].height; + } + for (uint32_t i = 0; i < ws_ui_prepass_count; i++) { + const uint32_t r = (uint32_t)groups[i].root; + int32_t y0 = groups[i].y, y1 = groups[i].y + groups[i].height; + if (r < ws_ui_prepass_count && run_y0[r] <= run_y1[r]) { + y0 = run_y0[r]; + y1 = run_y1[r]; + } + ws_ui_prepass[i].anchor_y = (y0 + y1) / 2 < half ? y0 : y1; + } + } /* A shared counter can comprise many glyphs and shadow packets, none of * which individually crosses the split. Classify the complete admitted @@ -6841,11 +6921,12 @@ static void gp0_dispatch_command(uint8_t opcode) { uint16_t color = rgb888_to_rgb555(gp0_cmd_buf[0] & 0xFFFFFFu); int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); - int ws_w = ws_sprt_fixed_transform(&x0, y0, 16); + int ws_h = 16; + int ws_w = ws_sprt_fixed_transform(&x0, &y0, 16, &ws_h); x0 += ws_nw_hud_shift(x0, 16); x0 += draw_offset_x; y0 += draw_offset_y; gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_flat_rect(x0, y0, (ws_w && ws_w != 16) ? ws_w : 16, 16, + gr_draw_flat_rect(x0, y0, (ws_w && ws_w != 16) ? ws_w : 16, ws_h, color); break; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index aea03933..28a285b5 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1510,6 +1510,15 @@ static bool netplay_local_viewport_native_wide(void) { return g_netplay_local_viewport == 1 && !g_netplay_local_viewport_projection; } +/* [widescreen] auto_ui_size from game.toml; a mod may override it per session + * (psx_mod_set_widescreen_hud_size), reset_mod_owned_presentation restores. */ +static bool g_ws_auto_ui_proportional_cfg = false; +extern "C" int psx_mod_set_widescreen_hud_size(int proportional) { + if (proportional != 0 && proportional != 1) return 0; + gpu_ws_set_auto_ui_proportional(proportional); + return 1; +} + extern "C" int psx_mod_set_fixed_display_aspect( uint32_t numerator, uint32_t denominator) { if (numerator == 0 || denominator == 0 || @@ -1618,6 +1627,7 @@ static void reset_mod_owned_presentation(void) { g_ws_adaptive_view = false; g_ws_adaptive_max_num = 16; g_ws_adaptive_max_den = 9; + gpu_ws_set_auto_ui_proportional(g_ws_auto_ui_proportional_cfg ? 1 : 0); psx_mod_set_world_scene_predicate(nullptr); gpu_ws_set_native_scene_predicate(nullptr); psx_mod_set_retained_scene_predicate(nullptr); @@ -14699,6 +14709,8 @@ int main(int argc, char** argv) { g_ws_hud_sprt = gc.ws_hud_sprt_squash; gpu_ws_set_auto_ui_squash(gc.ws_auto_ui_squash ? 1 : 0); gpu_ws_set_auto_ui_in_place(gc.ws_auto_ui_in_place ? 1 : 0); + g_ws_auto_ui_proportional_cfg = gc.ws_auto_ui_proportional; + gpu_ws_set_auto_ui_proportional(g_ws_auto_ui_proportional_cfg ? 1 : 0); /* [widescreen] full_2d — opt a pure-2D sprite game (MMX6) into the * widescreen present path. Applied to the GPU layer up front so the * ws engage at game entry classifies every frame as gameplay. */ diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 43e7a138..dbcb8e01 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -81,20 +81,21 @@ static int g_test_precise_triangles = 0; static int g_test_textured_triangles = 0; /* Bounding box of every flat/gouraud triangle drawn since the last reset. */ -static struct { int calls, min_x, max_x; } gpu_exec_triangles; +static struct { int calls, min_x, max_x, min_y, max_y; } gpu_exec_triangles; static inline void gpu_exec_reset_triangles(void) { gpu_exec_triangles.calls = 0; - gpu_exec_triangles.min_x = 1 << 30; - gpu_exec_triangles.max_x = -(1 << 30); + gpu_exec_triangles.min_x = gpu_exec_triangles.min_y = 1 << 30; + gpu_exec_triangles.max_x = gpu_exec_triangles.max_y = -(1 << 30); } static inline void gpu_exec_record_triangle(int x0, int y0, int x1, int y1, int x2, int y2) { - const int xs[3] = {x0, x1, x2}; - (void)y0; (void)y1; (void)y2; + const int xs[3] = {x0, x1, x2}, ys[3] = {y0, y1, y2}; gpu_exec_triangles.calls++; for (int i = 0; i < 3; i++) { if (xs[i] < gpu_exec_triangles.min_x) gpu_exec_triangles.min_x = xs[i]; if (xs[i] > gpu_exec_triangles.max_x) gpu_exec_triangles.max_x = xs[i]; + if (ys[i] < gpu_exec_triangles.min_y) gpu_exec_triangles.min_y = ys[i]; + if (ys[i] > gpu_exec_triangles.max_y) gpu_exec_triangles.max_y = ys[i]; } } diff --git a/runtime/tests/test_ws_auto_ui_untextured_exec.c b/runtime/tests/test_ws_auto_ui_untextured_exec.c index efa5a0fe..2dc60a80 100644 --- a/runtime/tests/test_ws_auto_ui_untextured_exec.c +++ b/runtime/tests/test_ws_auto_ui_untextured_exec.c @@ -130,6 +130,7 @@ static void reset_state(int in_place) { ws_full_2d = 0; gpu_ws_set_auto_ui_squash(1); gpu_ws_set_auto_ui_in_place(in_place); + gpu_ws_set_auto_ui_proportional(0); } /* Prepass the list and execute the two fills; returns their drawn X span. */ @@ -306,6 +307,36 @@ int main(void) { assert(gmax == ws_scale_about(120, left)); assert(fmin == gmin && fmax == gmax); + /* auto_ui_size proportional at 32:9 (squash 3/8): the HUD shrinks by + * sqrt((4/3) * 3/8) = sqrt(1/2) on both axes about its anchors. The run + * spans y 20..36 in the top half, so it keeps its top edge (y 20). */ + reset_state(0); + gpu_ws_set_auto_ui_proportional(1); + build_hud(60, 128, 64, 120); + run_fills(&gmin, &gmax, &fmin, &fmax); + { + const double s = sqrt(0.5); + const int32_t left = ws_disp_x(); + assert(gmin == left + (int32_t)lround((64 - left) * 3.0 / 8.0 * s)); + assert(gmax == left + (int32_t)lround((120 - left) * 3.0 / 8.0 * s)); + gpu_exec_reset_triangles(); + load_packet(NODE_FILL, 8); + gp0_exec_shaded_quad(); + assert(gpu_exec_triangles.min_y == 20 + (int32_t)lround(6 * s)); + assert(gpu_exec_triangles.max_y == 20 + (int32_t)lround(10 * s)); + } + /* At 16:9 (squash 3/4) proportional is identical to original. */ + reset_state(0); + ws_cfg_num = 16; ws_cfg_den = 9; ws_xnum = 3; ws_xden = 4; + gpu_ws_set_auto_ui_proportional(1); + build_hud(60, 128, 64, 120); + run_fills(&gmin, &gmax, &fmin, &fmax); + assert(gmin == ws_scale_about(64, ws_disp_x())); + gpu_exec_reset_triangles(); + load_packet(NODE_FILL, 8); + gp0_exec_shaded_quad(); + assert(gpu_exec_triangles.min_y == 26 && gpu_exec_triangles.max_y == 30); + /* A non-axis-aligned untextured quad reaching outside every widget is * world geometry, never UI (one inside a widget is a part of it: see the * needle below). */