From 9a8cdb6772f165f4cdbb07613ee8a7ef5cacfab4 Mon Sep 17 00:00:00 2001 From: douglasjv Date: Sat, 11 Jul 2026 08:45:46 -0700 Subject: [PATCH] Add typed native-wide textured edge expansion --- .../upstream/douglas-pr15-textured-edges.md | 10 +++++++ recompiler/src/config_loader.cpp | 18 +++++++++++++ recompiler/src/config_loader.h | 11 ++++++++ runtime/src/gpu.c | 11 ++++++++ runtime/src/gpu_gl_renderer.c | 26 +++++++++++++++---- runtime/src/main.cpp | 4 +++ 6 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 docs/internal/upstream/douglas-pr15-textured-edges.md diff --git a/docs/internal/upstream/douglas-pr15-textured-edges.md b/docs/internal/upstream/douglas-pr15-textured-edges.md new file mode 100644 index 00000000..b8a7acd5 --- /dev/null +++ b/docs/internal/upstream/douglas-pr15-textured-edges.md @@ -0,0 +1,10 @@ +# Typed native-wide textured edge expansion + +Source: [PSXRecomp PR #15](https://github.com/mstan/psxrecomp/pull/15), +commit [`cab3946855e74918d3ce006062d3e1b843b155d6`](https://github.com/douglasjv/psxrecomp-tweaks/commit/cab3946855e74918d3ce006062d3e1b843b155d6). + +This branch preserves the source feature for direct temperature testing. It is +opt-in. The known review question is software-renderer parity: the GL path +distinguishes outside-edge expansion from ordinary backdrop stretch, while the +software path currently consumes the gate as a boolean. Signed bounds and +title-specific configuration are excluded. diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index c894c71c..7c98b429 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -780,6 +780,9 @@ GameConfig load_game_config(const fs::path& config_path_in) { bool ws_clear_reveal = false; bool ws_nw_flat_backdrop = false; bool ws_nw_phase_backdrop = false; + bool ws_nw_textured_edges = false; + int ws_nw_textured_edge_scale = 0; + bool ws_nw_full_mirror = false; bool ws_offered = true; bool ws_ultrawide_offered = false; if (cfg.contains("widescreen")) { @@ -836,6 +839,18 @@ GameConfig load_game_config(const fs::path& config_path_in) { ws_nw_flat_backdrop = toml::find(ws, "nw_flat_backdrop"); if (ws.contains("nw_phase_backdrop")) ws_nw_phase_backdrop = toml::find(ws, "nw_phase_backdrop"); + if (ws.contains("nw_textured_edges")) + ws_nw_textured_edges = toml::find(ws, "nw_textured_edges"); + if (ws.contains("nw_textured_edge_scale")) { + ws_nw_textured_edge_scale = toml::find(ws, "nw_textured_edge_scale"); + if (ws_nw_textured_edge_scale != 0 && + (ws_nw_textured_edge_scale < 100 || ws_nw_textured_edge_scale > 400)) + throw std::runtime_error(fmt::format( + "{}: [widescreen] nw_textured_edge_scale must be 0 or in [100, 400]", + config_path.string())); + } + if (ws.contains("nw_full_mirror")) + ws_nw_full_mirror = toml::find(ws, "nw_full_mirror"); if (ws.contains("offer")) ws_offered = toml::find(ws, "offer"); if (ws.contains("offer_ultrawide")) @@ -1026,6 +1041,9 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_clear_reveal*/ ws_clear_reveal, /*ws_nw_flat_backdrop*/ ws_nw_flat_backdrop, /*ws_nw_phase_backdrop*/ ws_nw_phase_backdrop, + /*ws_nw_textured_edges*/ ws_nw_textured_edges, + /*ws_nw_textured_edge_scale*/ ws_nw_textured_edge_scale, + /*ws_nw_full_mirror*/ ws_nw_full_mirror, /*ws_offered*/ ws_offered, /*ws_ultrawide_offered*/ ws_ultrawide_offered, /*ws_bg2d_count_site*/ ws_bg2d_count_site, diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index e7f3b87e..1c6292d7 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -559,6 +559,17 @@ struct GameConfig { // off by default because draw ordering is title-specific. bool ws_nw_phase_backdrop = false; + // Expand only textured polygon vertices that already lie beyond the + // canonical 4:3 boundary. Useful for finite arena/background meshes while + // leaving actors, HUD, sprites, and centre geometry untouched. + bool ws_nw_textured_edges = false; + int ws_nw_textured_edge_scale = 0; // percent; 0 = aspect-derived + + // Render the complete wide mirror instead of splicing its centre from + // canonical VRAM. Required when edge-crossing polygon interpolation is + // transformed in the mirror. + bool ws_nw_full_mirror = false; + // [widescreen] offer — whether the launcher OFFERS its EXPERIMENTAL // Widescreen toggle for this title. Default true. Set false while a // title's widescreen is unported/unvalidated (e.g. MMX4 at bring-up): diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 5698a115..5178609e 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1185,6 +1185,12 @@ static int ws_tagged_anchor(int32_t *out_ax) { uint32_t g_ws_backdrop_lo = 0, g_ws_backdrop_hi = 0; static int ws_nw_phase_backdrop = 0; void gpu_ws_set_nw_phase_backdrop(int on) { ws_nw_phase_backdrop = on ? 1 : 0; } +static int ws_nw_textured_edges = 0; +int g_ws_tex_edge_pct = 0; +void gpu_ws_set_nw_textured_edges(int on, int scale_pct) { + ws_nw_textured_edges = on ? 1 : 0; + g_ws_tex_edge_pct = scale_pct; +} /* diag: per-frame min/max of the prim source addrs the GL gate sees */ uint32_t g_bdg_src_lo = 0xFFFFFFFFu, g_bdg_src_hi = 0; static uint32_t bdg_src_frame = 0xFFFFFFFFu; @@ -1270,6 +1276,11 @@ int psx_ws_prim_in_backdrop(void) { if (gp0_cmd_source_addr > g_bdg_src_hi) g_bdg_src_hi = gp0_cmd_source_addr; } if (g_dbg_mode != 0) return dbg_gate_match(); /* correlation override */ + if (ws_nw_textured_edges) { + uint32_t op = (gp0_cmd_buf[0] >> 24) & 0xFFu; + if (op >= 0x20u && op <= 0x3Fu && (op & 0x04u)) + return 2; /* GL gate: expand only vertices beyond canonical edges */ + } if (ws_nw_phase_backdrop && !ws_bg_phase_over()) return 1; /* Real gate: stretch the 2D backdrop = sprite-tagged prims drawn in the * background phase (before the 3D world). Fills the native-wide void for both diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 7c6ab01e..38bd6afb 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -754,7 +754,12 @@ static const char *GEO_VS = "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" "noperspective out vec4 v_col;\n" "void main(){ v_col = a_col;\n" - " float xb = (a_pos.x - u_xcenter)*u_xscale + u_xcenter;\n" + " float xb = a_pos.x;\n" + " if (u_xscale < 0.0) {\n" + " float s = -u_xscale; float h = u_xhalf / s;\n" + " float l = u_xcenter - h, r = u_xcenter + h;\n" + " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" + " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" " gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; static const char *GEO_FS = "#version 330\n" @@ -798,7 +803,12 @@ static const char *TEX_VS = " v_limits = ivec4(floor(a_limits + 0.5));\n" " /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n" " * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n" - " float xb = (a_pos.x - u_xcenter)*u_xscale + u_xcenter;\n" + " float xb = a_pos.x;\n" + " if (u_xscale < 0.0) {\n" + " float s = -u_xscale; float h = u_xhalf / s;\n" + " float l = u_xcenter - h, r = u_xcenter + h;\n" + " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" + " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" " gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; static const char *TEX_FS = "#version 330\n" @@ -1370,13 +1380,19 @@ static int mirror_geo_center_only(const int *xs, int n) { /* Set / clear the 2D-backdrop x-stretch for a wide-mirror draw, per the current * gate (s_bd_gate, set by the caller from bd_prim_gate / the batch gate). */ -static void wide_set_bd_scale(GLint uScale, GLint uCenter) { +static void wide_set_bd_scale(GLint uScale, GLint uCenter) { + extern int g_ws_tex_edge_pct; float scale = 1.0f, center = 0.0f; if (s_bd_gate && g_ws_bd_stretch_on && g_wide_w > 0) { int native_w = g_wide_w - 2 * g_wide_off; if (native_w > 0) { - scale = g_ws_bd_stretch_pct > 0 ? (float)g_ws_bd_stretch_pct / 100.0f - : (float)g_wide_w / (float)native_w; + scale = g_ws_bd_stretch_pct > 0 ? (float)g_ws_bd_stretch_pct / 100.0f + : (float)g_wide_w / (float)native_w; + if (s_bd_gate == 2) { + scale = g_ws_tex_edge_pct > 0 + ? (float)g_ws_tex_edge_pct / 100.0f : scale; + scale = -scale; /* shader: expand only beyond canonical edges */ + } center = (float)g_wide_cur_base + (float)native_w / 2.0f; } } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 5963e2a7..404a6900 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -346,6 +346,7 @@ static bool g_ws_hud_sprt = false; /* Runtime-only transition cleanup; kept out of gpu.h because generated game * units include that ABI header and do not need this frontend-only setter. */ extern "C" void gpu_ws_set_clear_reveal(int on); +extern "C" void gpu_ws_set_nw_textured_edges(int on, int scale_pct); /* Widescreen engages at game entry (fntrace_is_game_started): the BIOS boot * — Sony logo, PS logo, shell — presents authentic 4:3 with no GTE squash. * Starts true when the configured aspect is already 4:3 (nothing to engage). */ @@ -2844,6 +2845,9 @@ int main(int argc, char** argv) { /* [widescreen] nw_phase_backdrop — stretch only the textured * backdrop phase emitted before shaded 3D foreground geometry. */ gpu_ws_set_nw_phase_backdrop(gc.ws_nw_phase_backdrop ? 1 : 0); + gpu_ws_set_nw_textured_edges(gc.ws_nw_textured_edges ? 1 : 0, + gc.ws_nw_textured_edge_scale); + gl_renderer_set_wide_fast(gc.ws_nw_full_mirror ? 0 : 1); /* [widescreen] clear_reveal — enable opted-in scene/map-boundary * cleanup of synthetic native-wide margins. */ gpu_ws_set_clear_reveal(gc.ws_clear_reveal ? 1 : 0);