From 80c55b2dfad4508fb1a89142507bfa2438c055fd Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 14 Jul 2026 22:39:46 -0700 Subject: [PATCH 1/6] fix: preserve parameterless Play on data tracks --- runtime/src/cdrom.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 7f153b76..589d7948 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1536,10 +1536,23 @@ static void exec_command(uint8_t cmd) { if (param_count >= 1 && param_fifo[0] != 0) requested_track = bcd_to_bin(param_fifo[0]); if (!start_cdda_playback(requested_track)) { - response_push(stat_reg | CDSTAT_ERROR); - response_push(0x10); /* invalid track / position */ - set_irq(CDIRQ_ERROR); - break; + if (requested_track == 0) { + /* Some games probe Play from a data-track SetLoc before + * selecting their real audio track. The previous CD model + * acknowledged that command and entered a silent PLAY + * state; rejecting it makes Tomba 2 reset and retry its CD + * initialization forever. Preserve that compatibility + * behavior without feeding data sectors to the SPU. */ + stop_read_stream(); + cdda_data_end_pending = 0; + stat_reg = (stat_reg & ~(CDSTAT_SEEK | CDSTAT_READ)) | + CDSTAT_MOTOR | CDSTAT_PLAY; + } else { + response_push(stat_reg | CDSTAT_ERROR); + response_push(0x10); /* invalid explicit track */ + set_irq(CDIRQ_ERROR); + break; + } } } response_push(stat_reg); From 9a8cdb6772f165f4cdbb07613ee8a7ef5cacfab4 Mon Sep 17 00:00:00 2001 From: douglasjv Date: Sat, 11 Jul 2026 08:45:46 -0700 Subject: [PATCH 2/6] 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); From d7284bc4d49177232bad06406a6b59b8c222304d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 14 Jul 2026 21:46:25 -0700 Subject: [PATCH 3/6] build: declare GL wide-fast control --- runtime/include/gpu_gl_renderer.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 991e383a..401feb0c 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -67,6 +67,10 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) * for the widescreen field-of-view hack. */ void gl_renderer_set_display_aspect(int num, int den); +/* Select full native-wide mirror rendering instead of the centre-splice fast + * path. Textured edge expansion needs the complete mirror surface. */ +void gl_renderer_set_wide_fast(int on); + void gl_renderer_shutdown(void); /* Diagnostics (debug server): read GPU-side VRAM without touching the CPU From 28165d64212818ed47d1aa1da37addf1a6135f5b Mon Sep 17 00:00:00 2001 From: douglasjv Date: Thu, 9 Jul 2026 20:20:02 -0700 Subject: [PATCH 4/6] Expand Einhander player bounds for native widescreen --- .../upstream/douglas-pr15-signed-bounds.md | 10 +++++++ recompiler/include/code_generator.h | 2 ++ recompiler/src/code_generator.cpp | 16 ++++++++++ recompiler/src/config_loader.cpp | 21 ++++++++++++++ recompiler/src/config_loader.h | 8 +++++ recompiler/src/main_psx.cpp | 6 ++++ runtime/include/cpu_state.h | 1 + runtime/include/gpu.h | 6 ++++ runtime/include/overlay_api.h | 2 +- runtime/src/dirty_ram_interp.c | 5 +++- runtime/src/gpu.c | 29 +++++++++++++++++++ runtime/src/main.cpp | 12 ++++++++ runtime/src/overlay_sljit.c | 23 ++++++++++++--- 13 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 docs/internal/upstream/douglas-pr15-signed-bounds.md diff --git a/docs/internal/upstream/douglas-pr15-signed-bounds.md b/docs/internal/upstream/douglas-pr15-signed-bounds.md new file mode 100644 index 00000000..bf46f361 --- /dev/null +++ b/docs/internal/upstream/douglas-pr15-signed-bounds.md @@ -0,0 +1,10 @@ +# Typed widescreen signed bounds + +Source: [PSXRecomp PR #15](https://github.com/mstan/psxrecomp/pull/15), +commit [`f44f365eb91a30396f05f0f52529d4b01b026099`](https://github.com/douglasjv/psxrecomp-tweaks/commit/f44f365eb91a30396f05f0f52529d4b01b026099). + +This branch isolates the opt-in guarded LUI transform across static generation, +captured overlays, SLJIT, and the dirty interpreter. The source behavior is +parked for testing with two known review questions: runtime registration clamps +at 64 sites and scaling currently includes the renderer cull margin. Textured +edges, debug tooling, and title-specific addresses are excluded. diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index ea1960ef..b5ba0a31 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -9,6 +9,7 @@ #include "function_analysis.h" #include "control_flow.h" #include "../src/annotations.hpp" +#include "../src/config_loader.h" namespace PSXRecomp { @@ -43,6 +44,7 @@ struct CodeGenConfig { // mismatch is EXPECTED (apply the transform only where the bytes match, skip // it elsewhere) rather than a hard config error. bool overlay_mode = false; + std::vector ws_signed_x_bound_sites; std::string indent; // Indentation string (default: " ") // Widescreen sprite-tag hooks ([widescreen] sprite_tag_funcs): functions // that get a psx_ws_sprite_tag(cpu) callback emitted at entry, so the diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 5c9f5fde..367935a8 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -758,6 +758,21 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) std::string code; + for (const auto& site : config_.ws_signed_x_bound_sites) { + if ((site.address & 0x1FFFFFFFu) != (addr & 0x1FFFFFFFu)) continue; + if (instr != site.expected) { + if (config_.overlay_mode) continue; + fmt::print(stderr, + "ERROR: signed_x_bound expected 0x{:08X} at 0x{:08X}, found 0x{:08X}\n", + site.expected, addr, instr); + std::exit(1); + } + const int32_t vanilla = (int32_t)((instr & 0xFFFFu) << 16); + return fmt::format("{} = (uint32_t)psx_ws_player_x_bound((int32_t)0x{:08X});" + " /* typed native-wide signed X bound */{}", + reg_name(get_rt(instr)), (uint32_t)vanilla, comment); + } + // Widescreen automatic far-backdrop column PRELOAD ([widescreen.cull] // auto_backdrop). At a detected window's START/END finalize, route the value // through psx_ws_backdrop_value(orig, is_end): identity at 4:3, but in @@ -2635,6 +2650,7 @@ std::string CodeGenerator::generate_file( ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n"; ss << "extern void psx_ws_mmx6_bg_stage_init(void); /* ws 2D stage reveal invalidation (gpu.c) */\n"; ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n"; + ss << "extern int32_t psx_ws_player_x_bound(int32_t vanilla); /* typed gameplay X bound */\n"; ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n"; ss << "extern int psx_ws_cull_slti(uint32_t sx, uint32_t imm); /* ws cull signed right edge (gpu.c) */\n"; ss << "extern int psx_ws_cull_bltz(uint32_t v); /* ws cull signed left edge (gpu.c) */\n"; diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 7c98b429..97f4c719 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -783,6 +783,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { bool ws_nw_textured_edges = false; int ws_nw_textured_edge_scale = 0; bool ws_nw_full_mirror = false; + std::vector ws_signed_x_bound_sites; bool ws_offered = true; bool ws_ultrawide_offered = false; if (cfg.contains("widescreen")) { @@ -851,6 +852,25 @@ GameConfig load_game_config(const fs::path& config_path_in) { } if (ws.contains("nw_full_mirror")) ws_nw_full_mirror = toml::find(ws, "nw_full_mirror"); + if (ws.contains("signed_x_bound")) { + std::set seen; + for (const auto& item : toml::find(ws, "signed_x_bound")) { + WidescreenSignedBoundSite site; + site.address = parse_hex(toml::find(item, "address"), + "widescreen.signed_x_bound.address"); + site.expected = parse_hex(toml::find(item, "expected"), + "widescreen.signed_x_bound.expected"); + if ((site.expected >> 26) != 0x0Fu) + throw std::runtime_error(fmt::format( + "{}: [[widescreen.signed_x_bound]] expected must be LUI", + config_path.string())); + if (!seen.insert(site.address & 0x1FFFFFFFu).second) + throw std::runtime_error(fmt::format( + "{}: duplicate [[widescreen.signed_x_bound]] address 0x{:08X}", + config_path.string(), site.address)); + ws_signed_x_bound_sites.push_back(site); + } + } if (ws.contains("offer")) ws_offered = toml::find(ws, "offer"); if (ws.contains("offer_ultrawide")) @@ -1044,6 +1064,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*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_signed_x_bound_sites*/ ws_signed_x_bound_sites, /*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 1c6292d7..99169fba 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -39,6 +39,10 @@ namespace PSXRecompV4 { // digital — always present a digital pad (id 0x41); sticks disabled. enum PadMode { PAD_MODE_HYBRID = 0, PAD_MODE_ANALOG = 1, PAD_MODE_DIGITAL = 2 }; +struct WidescreenSignedBoundSite { + uint32_t address = 0; + uint32_t expected = 0; // guarded LUI instruction +}; // Parse/format a pad mode. pad_mode_from_string accepts "hybrid"/"analog"/ // "digital" (case-insensitive) and returns `fallback` for anything else. int pad_mode_from_string(const std::string& s, int fallback); @@ -570,6 +574,10 @@ struct GameConfig { // transformed in the mirror. bool ws_nw_full_mirror = false; + // [[widescreen.signed_x_bound]] guarded LUI sites whose signed Q16 + // constants scale with the active native-wide field and remain identity in + // 4:3/menus/FMV. Shared by static codegen, overlay JIT, and interpreter. + std::vector ws_signed_x_bound_sites; // [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/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index c93358d4..83a7c611 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -131,6 +131,7 @@ int main(int argc, char** argv) { bool ws_auto_screen_x_cull = false; // [widescreen.cull] auto_screen_x std::set persist_init_sites; // [persist_options] init-store hooks (game_options.toml) std::vector instruction_patches; + std::vector ws_signed_x_bound_sites; bool ws_auto_backdrop_preload = false; // [widescreen.cull] auto_backdrop uint32_t ws_bg2d_count_site = 0, ws_bg2d_startcol_site = 0, ws_bg2d_startx_site = 0, @@ -172,6 +173,7 @@ int main(int argc, char** argv) { if (cfg.ws_bg2d_bufbase_site) ws_bg2d_bufbase_site = cfg.ws_bg2d_bufbase_site; if (cfg.ws_bg2d_cap_site) ws_bg2d_cap_site = cfg.ws_bg2d_cap_site; if (cfg.ws_bg2d_init_func) ws_bg2d_init_func = cfg.ws_bg2d_init_func; + ws_signed_x_bound_sites = cfg.ws_signed_x_bound_sites; // [persist_options] init-store hook sites live in a dedicated // game_options.toml next to game.toml (the game's own native OPTION // settings, kept separate from game.toml/settings.toml). Best-effort: @@ -245,6 +247,9 @@ int main(int argc, char** argv) { ws_auto_backdrop_preload = ws_auto_backdrop_preload || wscfg.ws_auto_backdrop_preload; PSXRecompV4::merge_recompiler_patches( instruction_patches, wscfg.recompiler_patches); + ws_signed_x_bound_sites.insert(ws_signed_x_bound_sites.end(), + wscfg.ws_signed_x_bound_sites.begin(), + wscfg.ws_signed_x_bound_sites.end()); if (wscfg.ws_bg2d_init_func) ws_bg2d_init_func = wscfg.ws_bg2d_init_func; fmt::print("ws-config: {} (backdrop_x sites={}, unsquash funcs={})\n", ws_config_path.string(), ws_backdrop_x.size(), ws_backdrop_unsquash.size()); @@ -811,6 +816,7 @@ int main(int argc, char** argv) { codegen_config.emit_line_numbers = true; codegen_config.split_mid_function_targets = !overlay_mode; codegen_config.overlay_mode = overlay_mode; + codegen_config.ws_signed_x_bound_sites = ws_signed_x_bound_sites; codegen_config.ws_sprite_tag_funcs = ws_tag_funcs; codegen_config.ws_bg2d_init_func = ws_bg2d_init_func; codegen_config.ws_cull_bias_sites = ws_cull_bias; diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 095ba8a5..3110e6a1 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -33,6 +33,7 @@ enum { SLJIT_HLP_CALL, /* psx_sljit_call — jal/jalr call contract */ SLJIT_HLP_WS_CULL_SLTI, /* psx_ws_cull_slti — signed funnel right edge (appended) */ SLJIT_HLP_WS_CULL_BLTZ, /* psx_ws_cull_bltz — signed funnel left edge (appended) */ + SLJIT_HLP_WS_PLAYER_X_BOUND, /* typed native-wide signed gameplay bound */ SLJIT_HLP_COUNT }; diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index f615358e..48b7d39d 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -145,6 +145,12 @@ void gpu_ws_set_explicit_cull_sites(const uint32_t *bias, int nbias, const uint32_t *slti, int nslti); int psx_ws_is_cull_bias_site(uint32_t pc); int psx_ws_is_cull_slti_site(uint32_t pc); +/* Scale a signed Q16 horizontal gameplay limit into the active native-wide + * game field. Identity at 4:3 / menus / FMV. */ +int32_t psx_ws_player_x_bound(int32_t vanilla); +void gpu_ws_set_signed_x_bound_sites(const uint32_t *addresses, + const uint32_t *expected, int count); +int psx_ws_is_signed_x_bound_site(uint32_t pc, uint32_t instr); /* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x): * the gcc emit, the sljit JIT, and the interpreter all route a flagged diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h index 92d8b60a..e1bec92d 100644 --- a/runtime/include/overlay_api.h +++ b/runtime/include/overlay_api.h @@ -87,7 +87,7 @@ * 4: continuation-passing (PSX_CPS, RECURSION_BUG.md §25) — overlay funcs * tail-transfer + carry an entry-switch; the loader routes continuations. * Fresh namespace so CPS DLLs never reuse a legacy (unit-model) cg3 DLL. */ -#define PSX_OVERLAY_CODEGEN_VER 4 +#define PSX_OVERLAY_CODEGEN_VER 5 /* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen * sources). Folded into the cache PATH next to cg (gcc//cg_/) diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 6e6889d4..ea8ef0d8 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1575,7 +1575,10 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) { cpu->gpr[0] = 0; return 0; case 0x0F: /* LUI rt, imm */ - cpu->gpr[rt] = imm << 16; + if (psx_ws_is_signed_x_bound_site(pc, insn)) + cpu->gpr[rt] = (uint32_t)psx_ws_player_x_bound((int32_t)(imm << 16)); + else + cpu->gpr[rt] = imm << 16; cpu->gpr[0] = 0; return 0; case 0x10: { /* COP0 */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 5178609e..1509edb6 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -347,6 +347,35 @@ int psx_ws_x_margin(void) { return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum; } +int32_t psx_ws_player_x_bound(int32_t vanilla) +{ + const int margin = psx_ws_x_margin(); + if (margin <= 0) return vanilla; + return (int32_t)(((int64_t)vanilla * (320 + 2 * margin)) / 320); +} + +#define WS_SIGNED_BOUND_MAX 64 +static uint32_t ws_signed_bound_addr[WS_SIGNED_BOUND_MAX]; +static uint32_t ws_signed_bound_expected[WS_SIGNED_BOUND_MAX]; +static int ws_signed_bound_count = 0; +void gpu_ws_set_signed_x_bound_sites(const uint32_t *addresses, + const uint32_t *expected, int count) { + if (count < 0) count = 0; + if (count > WS_SIGNED_BOUND_MAX) count = WS_SIGNED_BOUND_MAX; + ws_signed_bound_count = count; + for (int i = 0; i < count; i++) { + ws_signed_bound_addr[i] = addresses[i] & 0x1FFFFFFFu; + ws_signed_bound_expected[i] = expected[i]; + } +} +int psx_ws_is_signed_x_bound_site(uint32_t pc, uint32_t instr) { + const uint32_t phys = pc & 0x1FFFFFFFu; + for (int i = 0; i < ws_signed_bound_count; i++) + if (ws_signed_bound_addr[i] == phys && ws_signed_bound_expected[i] == instr) + return 1; + return 0; +} + /* ---- Capcom 2D background tile-loop widen ([widescreen.bg2d]) -------------- * Mega Man X5/X6 use a pure-2D sprite engine that renders only a 4:3 (320px) field * of view — there is no overscan to "reveal." Its per-layer background renderer diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 404a6900..680ee5f1 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -347,6 +347,7 @@ static bool g_ws_hud_sprt = false; * 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); +extern "C" void gpu_ws_set_signed_x_bound_sites(const uint32_t*, const uint32_t*, int); /* 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). */ @@ -2848,6 +2849,17 @@ int main(int argc, char** argv) { 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); + { + std::vector addresses, expected; + addresses.reserve(gc.ws_signed_x_bound_sites.size()); + expected.reserve(gc.ws_signed_x_bound_sites.size()); + for (const auto& site : gc.ws_signed_x_bound_sites) { + addresses.push_back(site.address); + expected.push_back(site.expected); + } + gpu_ws_set_signed_x_bound_sites(addresses.data(), expected.data(), + (int)addresses.size()); + } /* [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); diff --git a/runtime/src/overlay_sljit.c b/runtime/src/overlay_sljit.c index 66e39a69..4512dae5 100644 --- a/runtime/src/overlay_sljit.c +++ b/runtime/src/overlay_sljit.c @@ -163,6 +163,7 @@ static uint32_t s_gpr_lru; static int s_frag_ws_cull; extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* gpu.c — shared widen */ extern int psx_ws_cull_slti(uint32_t sx, uint32_t imm); /* gpu.c — signed right edge */ +extern int32_t psx_ws_player_x_bound(int32_t vanilla); /* gpu.c — gameplay X clamp */ extern int psx_ws_cull_bltz(uint32_t v); /* gpu.c — signed left edge */ extern int psx_ws_is_cull_w_imm(uint32_t imm); /* gpu.c — per-game W-imm set */ extern int psx_ws_auto_cull_on(void); /* gpu.c — per-game opt-in gate */ @@ -371,6 +372,17 @@ void overlay_sljit_init_helpers(CPUState *cpu) { cpu->sljit_helpers[SLJIT_HLP_CALL] = (void *)psx_sljit_call; cpu->sljit_helpers[SLJIT_HLP_WS_CULL_SLTI] = (void *)psx_ws_cull_slti; cpu->sljit_helpers[SLJIT_HLP_WS_CULL_BLTZ] = (void *)psx_ws_cull_bltz; + cpu->sljit_helpers[SLJIT_HLP_WS_PLAYER_X_BOUND] = (void *)psx_ws_player_x_bound; +} + +static void emit_ws_player_x_bound(struct sljit_compiler *C, uint32_t rt, int32_t vanilla) { + sljit_emit_op1(C, SLJIT_MOV32, SLJIT_R0, 0, SLJIT_IMM, (sljit_sw)vanilla); + sljit_emit_op1(C, SLJIT_MOV, SLJIT_R2, 0, SLJIT_MEM1(R_CPU), + (sljit_sw)(offsetof(CPUState, sljit_helpers) + + (size_t)SLJIT_HLP_WS_PLAYER_X_BOUND * sizeof(void *))); + sljit_emit_icall(C, SLJIT_CALL, SLJIT_ARGS1(32, 32), SLJIT_R2, 0); + sljit_emit_op1(C, SLJIT_MOV32, gpr_dst(C, rt), 0, SLJIT_R0, 0); + gpr_unpin(); } /* Emit `rt = (gpr[rs], imm)` for a flagged auto_screen_x cull @@ -401,7 +413,7 @@ enum { EMIT_OK = 0, EMIT_TERM = 1, EMIT_ABORT = 2 }; * is `jr $ra` (the only terminator this slice supports — caller then emits the * delay slot + the shard return), or EMIT_ABORT for anything outside the slice * (any other control transfer, or an unsupported opcode). */ -static int emit_one(struct sljit_compiler *C, uint32_t insn, int bd_kind, int bd_cols) { +static int emit_one(struct sljit_compiler *C, uint32_t pc, uint32_t insn, int bd_kind, int bd_cols) { uint32_t op = f_op(insn), rs = f_rs(insn), rt = f_rt(insn); uint32_t rd = f_rd(insn), sh = f_sh(insn), fn = f_fn(insn); uint32_t imm = f_imm(insn); @@ -615,7 +627,10 @@ static int emit_one(struct sljit_compiler *C, uint32_t insn, int bd_kind, int bd return EMIT_OK; } case 0x0F: /* LUI rt = imm << 16 */ - sljit_emit_op1(C, SLJIT_MOV32, gpr_dst(C, rt), 0, SLJIT_IMM, (sljit_sw)(imm << 16)); + if (psx_ws_is_signed_x_bound_site(pc, insn)) + emit_ws_player_x_bound(C, rt, (int32_t)(imm << 16)); + else + sljit_emit_op1(C, SLJIT_MOV32, gpr_dst(C, rt), 0, SLJIT_IMM, (sljit_sw)(imm << 16)); return EMIT_OK; case 0x20: emit_load(C, offsetof(CPUState, read_byte), SLJIT_MOV_S8, rt, rs, simm); return EMIT_OK; /* LB */ case 0x21: emit_load(C, offsetof(CPUState, read_half), SLJIT_MOV_S16, rt, rs, simm); return EMIT_OK; /* LH */ @@ -996,7 +1011,7 @@ void overlay_sljit_try_compile(uint32_t entry, pending = PEND_CALL; continue; } - if (emit_one(C, insn, s_bd_kind[i], s_bd_cols[i]) != EMIT_OK) { aborted = 1; break; } + if (emit_one(C, entry + i * 4u, insn, s_bd_kind[i], s_bd_cols[i]) != EMIT_OK) { aborted = 1; break; } } else { /* This instruction is the delay slot of the pending control insn; * it must be a plain, supported, non-control op (the constraint the @@ -1006,7 +1021,7 @@ void overlay_sljit_try_compile(uint32_t entry, * the pending transfer. */ int32_t dummy = 0; if (classify_control(insn, i * 4u, entry_phys, &dummy) != CTRL_NONE) { aborted = 1; break; } - if (emit_one(C, insn, s_bd_kind[i], s_bd_cols[i]) != EMIT_OK) { aborted = 1; break; } + if (emit_one(C, entry + i * 4u, insn, s_bd_kind[i], s_bd_cols[i]) != EMIT_OK) { aborted = 1; break; } gpr_flush(C); if (pending == PEND_RET) { if (g_psx_cps_mode) { From 2ceaf5abee183bd00f9a9f8d967a161502c6eab9 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 14 Jul 2026 10:11:20 -0700 Subject: [PATCH 5/6] feat: park projection and perspective correction Adapted from https://github.com/mstan/psxrecomp/commit/ec889d6e5956fc4f34acfee41d87f30b24123b7f Co-authored-by: Kareem Olim --- .../kem0x-pr14-projection-perspective.md | 22 +++ recompiler/src/code_generator.cpp | 11 +- recompiler/src/strict_translator.cpp | 12 +- runtime/include/cpu_state.h | 1 + runtime/include/gpu_sw_renderer.h | 14 ++ runtime/src/dirty_ram_interp.c | 1 + runtime/src/gpu.c | 139 +++++++++++++- runtime/src/gpu_sw_renderer.c | 174 +++++++++++++++--- runtime/src/gte.cpp | 155 +++++++++++++++- runtime/src/memory.c | 4 + runtime/src/overlay_loader.c | 2 +- runtime/src/psx_interpreter.c | 1 + 12 files changed, 492 insertions(+), 44 deletions(-) create mode 100644 docs/internal/upstream/kem0x-pr14-projection-perspective.md diff --git a/docs/internal/upstream/kem0x-pr14-projection-perspective.md b/docs/internal/upstream/kem0x-pr14-projection-perspective.md new file mode 100644 index 00000000..ecd27289 --- /dev/null +++ b/docs/internal/upstream/kem0x-pr14-projection-perspective.md @@ -0,0 +1,22 @@ +# PR #14 projection precision and perspective textures + +Source: [kem0x PR #14](https://github.com/mstan/psxrecomp/pull/14), commit +[`ec889d6e5956fc4f34acfee41d87f30b24123b7f`](https://github.com/mstan/psxrecomp/commit/ec889d6e5956fc4f34acfee41d87f30b24123b7f). + +This branch parks two coupled, opt-in visual experiments together: + +- retain discarded GTE 16.16 screen fractions and use exact packed-coordinate + matches for supersampled software-renderer vertices; +- track exact SWC2 RAM provenance through generated, strict, dirty-RAM, + overlay, and fallback interpreters so textured world polygons can use + perspective-correct UV interpolation without guessing from screen position. + +They share the same projection side cache, GP0 triangle preparation, and +software-renderer consumption path; splitting those internals would duplicate +most of the risk-bearing machinery. Both remain disabled until their existing +generic setters are called. + +Excluded: all Web host exports and every title-specific state, action, address, +or presentation hook from the source commit. The separate 60 Hz presentation +experiment is parked on its own branch. Authorship is retained in the commit +trailer. diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 367935a8..61acb674 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -1295,11 +1295,14 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) : fmt::format("cpu->gte_data[{}]", rt); std::string swc2_store_pc = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", addr); if (offset == 0) { - code = gte_stall + swc2_store_pc + fmt::format("cpu->write_word({}, {}); /* swc2 gte[{}], ({}) */", - reg_name(rs), value, rt, reg_name(rs)); + code = gte_stall + swc2_store_pc + fmt::format( + "cpu->write_word({}, {}); gte_precision_store_word({}, {}); /* swc2 gte[{}], ({}) */", + reg_name(rs), value, reg_name(rs), rt, rt, reg_name(rs)); } else { - code = gte_stall + swc2_store_pc + fmt::format("cpu->write_word({} + {}, {}); /* swc2 gte[{}], {}({}) */", - reg_name(rs), offset, value, rt, offset, reg_name(rs)); + code = gte_stall + swc2_store_pc + fmt::format( + "cpu->write_word({} + {}, {}); gte_precision_store_word({} + {}, {}); /* swc2 gte[{}], {}({}) */", + reg_name(rs), offset, value, reg_name(rs), offset, rt, + rt, offset, reg_name(rs)); } } break; diff --git a/recompiler/src/strict_translator.cpp b/recompiler/src/strict_translator.cpp index 6f7ca4b1..5e4456b2 100644 --- a/recompiler/src/strict_translator.cpp +++ b/recompiler/src/strict_translator.cpp @@ -1386,13 +1386,17 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction& if (offset == 0) { r.c_code = gte_stall + fmt::format( "g_debug_last_store_pc = 0x{:08X}u; " - "cpu->write_word(cpu->gpr[{}], {});", - d.address, static_cast(rs), value); + "cpu->write_word(cpu->gpr[{}], {}); " + "gte_precision_store_word(cpu->gpr[{}], {});", + d.address, static_cast(rs), value, + static_cast(rs), static_cast(rt)); } else { r.c_code = gte_stall + fmt::format( "g_debug_last_store_pc = 0x{:08X}u; " - "cpu->write_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {});", - d.address, static_cast(rs), static_cast(offset), value); + "cpu->write_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {}); " + "gte_precision_store_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {});", + d.address, static_cast(rs), static_cast(offset), value, + static_cast(rs), static_cast(offset), static_cast(rt)); } r.comment = fmt::format("swc2 gte[{}], {}({})", rt, static_cast(offset), gpr_name(rs)); diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 3110e6a1..bc131bdd 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -190,6 +190,7 @@ extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg); extern uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg); extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val); extern void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val); +extern void gte_precision_store_word(uint32_t addr, uint8_t reg); /* ============================================================================ * Dispatch call contract (Bug D / wild-return family fix) diff --git a/runtime/include/gpu_sw_renderer.h b/runtime/include/gpu_sw_renderer.h index 8fe351bb..b8cea0e3 100644 --- a/runtime/include/gpu_sw_renderer.h +++ b/runtime/include/gpu_sw_renderer.h @@ -22,6 +22,20 @@ void sw_renderer_init(uint16_t* vram); void sw_renderer_set_scale(int scale); int sw_renderer_scale(void); +/* Visual-only high-resolution vertex override for the next triangle. Values + * are signed 16.16 screen coordinates; native VRAM still uses the integer GP0 + * positions, while the supersampled mirror consumes these fractions. */ +void sw_set_precise_triangle(int enabled, + int32_t x0, int32_t y0, + int32_t x1, int32_t y1, + int32_t x2, int32_t y2); + +/* Perspective-correct UV override for the next triangle. q values are the + * homogeneous interpolation weights associated with each screen vertex. */ +void sw_set_perspective_triangle(int enabled, + float q0, float q1, float q2); +uint32_t sw_perspective_triangle_count(void); + /* Texture filtering. 0 = nearest (native PSX look, default), 1 = bilinear * (smooths textures/2D backgrounds; blends in RGB after the CLUT lookup, * transparency-aware). Independent of supersampling. */ diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index ea8ef0d8..ecf0253c 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1739,6 +1739,7 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) { psx_gte_stall(cpu); /* COP2 reg read stalls to GTE completion */ #endif cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt)); + gte_precision_store_word(addr, (uint8_t)rt); return 0; } default: diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 1509edb6..fe07e162 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2082,6 +2082,75 @@ static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) { *y = sign_extend((word >> 16) & 0x7FFu, 11); } +extern int gte_geometry_correction_lookup(uint32_t packed, + int32_t *x16, int32_t *y16); +extern int gte_geometry_correction_enabled(void); +static int s_texture_correction_enabled = 0; +extern void gte_precision_tracking_set(int enabled); +extern int gte_precision_load_word(uint32_t addr, uint32_t packed, + int32_t *x16, int32_t *y16, uint16_t *z); + +void gpu_texture_correction_set(int enabled) { + s_texture_correction_enabled = enabled ? 1 : 0; + gte_precision_tracking_set(enabled); +} + +uint32_t gpu_texture_correction_hits(void) { + return sw_perspective_triangle_count(); +} + +/* Match all three GP0 positions to recent GTE projections. Requiring a full + * triangle match prevents screen-space HUD/sprites that happen to share one + * coordinate from receiving world-geometry correction. The integer delta + * folds in draw offsets and any widescreen adjustment already applied. */ +static void prepare_precise_triangle(uint32_t w0, uint32_t w1, uint32_t w2, + const int32_t vx[3], const int32_t vy[3]) { + uint32_t words[3] = { w0, w1, w2 }; + int32_t fx[3], fy[3]; + const int geometry_enabled = gte_geometry_correction_enabled(); + sw_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); + if (!geometry_enabled) { + sw_set_precise_triangle(0, 0,0, 0,0, 0,0); + return; + } + for (int i = 0; i < 3; i++) { + int32_t raw_x, raw_y; + parse_vertex(words[i], &raw_x, &raw_y); + if (!gte_geometry_correction_lookup(words[i], &fx[i], &fy[i])) { + sw_set_precise_triangle(0, 0,0, 0,0, 0,0); + return; + } + fx[i] = (int32_t)((int64_t)fx[i] + + (int64_t)(vx[i] - raw_x) * 65536); + fy[i] = (int32_t)((int64_t)fy[i] + + (int64_t)(vy[i] - raw_y) * 65536); + } + sw_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]); +} + +/* Enable perspective UVs only when every position word came from an exact + * SWC2 projection store at that same DMA packet address. This preserves the + * association through ordering-table reordering and rejects CPU-built UI. */ +static void prepare_texture_triangle(int i0, int i1, int i2) { + sw_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); + if (!s_texture_correction_enabled || gp0_cmd_source_addr == 0xFFFFFFFFu) + return; + int indices[3] = { i0, i1, i2 }; + uint16_t z[3]; + for (int i = 0; i < 3; i++) { + uint32_t addr = (gp0_cmd_source_addr + (uint32_t)indices[i] * 4u) & 0x1FFFFCu; + if (!gte_precision_load_word(addr, gp0_cmd_buf[indices[i]], NULL, NULL, &z[i]) || + z[i] == 0) + return; + } + float q[3] = { 1.0f / (float)z[0], 1.0f / (float)z[1], 1.0f / (float)z[2] }; + float qmax = q[0]; + if (q[1] > qmax) qmax = q[1]; + if (q[2] > qmax) qmax = q[2]; + if (qmax <= 0.0f) return; + sw_set_perspective_triangle(1, q[0] / qmax, q[1] / qmax, q[2] / qmax); +} + /* Write a single pixel to VRAM with draw area clipping and mask bit handling */ static void raster_pixel(int32_t x, int32_t y, uint16_t color) { if (x < (int32_t)draw_area_left || x > (int32_t)draw_area_right) return; @@ -2190,6 +2259,8 @@ static void gp0_exec_mono_tri(void) { vy[i] += draw_offset_y; } gr_set_semi_transparency(semi_trans, (int)semi_transparency); + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[2], gp0_cmd_buf[3], + vx, vy); gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); } @@ -2232,10 +2303,20 @@ static void gp0_exec_mono_quad(void) { vy[i] += draw_offset_y; } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - if (!rej_a) + if (!rej_a) { + int32_t tx[3] = { vx[0], vx[1], vx[2] }; + int32_t ty[3] = { vy[0], vy[1], vy[2] }; + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[2], + gp0_cmd_buf[3], tx, ty); gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); - if (!rej_b) + } + if (!rej_b) { + int32_t tx[3] = { vx[2], vx[1], vx[3] }; + int32_t ty[3] = { vy[2], vy[1], vy[3] }; + prepare_precise_triangle(gp0_cmd_buf[3], gp0_cmd_buf[2], + gp0_cmd_buf[4], tx, ty); gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); + } } /* Execute shaded triangle (GP0 0x30-0x33) — Gouraud shaded */ @@ -2255,6 +2336,8 @@ static void gp0_exec_shaded_tri(void) { vy[i] += draw_offset_y; } gr_set_semi_transparency(semi_trans, (int)semi_transparency); + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], gp0_cmd_buf[5], + vx, vy); gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], vx[2], vy[2], c[2]); @@ -2295,14 +2378,24 @@ static void gp0_exec_shaded_quad(void) { } } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - if (!rej_a) + if (!rej_a) { + int32_t tx[3] = { vx[0], vx[1], vx[2] }; + int32_t ty[3] = { vy[0], vy[1], vy[2] }; + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], + gp0_cmd_buf[5], tx, ty); gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], vx[2], vy[2], c[2]); - if (!rej_b) + } + if (!rej_b) { + int32_t tx[3] = { vx[2], vx[1], vx[3] }; + int32_t ty[3] = { vy[2], vy[1], vy[3] }; + prepare_precise_triangle(gp0_cmd_buf[5], gp0_cmd_buf[3], + gp0_cmd_buf[7], tx, ty); gr_draw_gouraud_triangle(vx[2], vy[2], c[2], vx[1], vy[1], c[1], vx[3], vy[3], c[3]); + } } /* Helper: build texpage word from GPU state for SW renderer. @@ -2368,6 +2461,9 @@ static void gp0_exec_textured_tri(void) { } setup_textured_draw(color24, semi_trans, raw_texture); + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], gp0_cmd_buf[5], + vx, vy); + prepare_texture_triangle(1, 3, 5); 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], @@ -2442,16 +2538,28 @@ static void gp0_exec_textured_quad(void) { } } - if (!rej_a) + if (!rej_a) { + int32_t tx[3] = { vx[0], vx[1], vx[2] }; + int32_t ty[3] = { vy[0], vy[1], vy[2] }; + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], + gp0_cmd_buf[5], tx, ty); + prepare_texture_triangle(1, 3, 5); 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], clut_x, clut_y, tpage); - if (!rej_b) + } + if (!rej_b) { + int32_t tx[3] = { vx[2], vx[1], vx[3] }; + int32_t ty[3] = { vy[2], vy[1], vy[3] }; + prepare_precise_triangle(gp0_cmd_buf[5], gp0_cmd_buf[3], + gp0_cmd_buf[7], tx, ty); + prepare_texture_triangle(5, 3, 7); 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], clut_x, clut_y, tpage); + } } /* Execute shaded textured triangle (GP0 0x34-0x37) */ @@ -2486,6 +2594,9 @@ static void gp0_exec_shaded_textured_tri(void) { } gr_set_semi_transparency(semi_trans, (int)semi_transparency); + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[4], gp0_cmd_buf[7], + vx, vy); + prepare_texture_triangle(1, 4, 7); 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], @@ -2529,16 +2640,28 @@ static void gp0_exec_shaded_textured_quad(void) { } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - if (!rej_a) + if (!rej_a) { + int32_t tx[3] = { vx[0], vx[1], vx[2] }; + int32_t ty[3] = { vy[0], vy[1], vy[2] }; + prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[4], + gp0_cmd_buf[7], tx, ty); + prepare_texture_triangle(1, 4, 7); 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], clut_x, clut_y, tpage, raw_texture); - if (!rej_b) + } + if (!rej_b) { + int32_t tx[3] = { vx[2], vx[1], vx[3] }; + int32_t ty[3] = { vy[2], vy[1], vy[3] }; + prepare_precise_triangle(gp0_cmd_buf[7], gp0_cmd_buf[4], + gp0_cmd_buf[10], tx, ty); + prepare_texture_triangle(7, 4, 10); 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], clut_x, clut_y, tpage, raw_texture); + } } /* Execute mono line (GP0 0x40-0x47) — Bresenham */ diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index df7b18ac..86b8459c 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -51,6 +51,11 @@ static uint16_t *g_hr = NULL; static int g_scale = 1; static int g_hr_w = VRAM_WIDTH; static int g_hr_h = VRAM_HEIGHT; +static int g_precise_valid = 0; +static int32_t g_precise_x16[3], g_precise_y16[3]; +static int g_perspective_valid = 0; +static float g_perspective_q[3]; +static uint32_t g_perspective_triangles = 0; /* ---- Native-wide compositor (separate present surfaces) ------------------ * Canonical VRAM stays faithful. For an opted-in wide game we ADDITIONALLY @@ -490,6 +495,49 @@ void sw_renderer_set_scale(int scale) { int sw_renderer_scale(void) { return g_scale; } +void sw_set_precise_triangle(int enabled, + int32_t x0, int32_t y0, + int32_t x1, int32_t y1, + int32_t x2, int32_t y2) { + g_precise_valid = enabled ? 1 : 0; + g_precise_x16[0] = x0; g_precise_y16[0] = y0; + g_precise_x16[1] = x1; g_precise_y16[1] = y1; + g_precise_x16[2] = x2; g_precise_y16[2] = y2; +} + +void sw_set_perspective_triangle(int enabled, + float q0, float q1, float q2) { + g_perspective_valid = enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f; + g_perspective_q[0] = q0; + g_perspective_q[1] = q1; + g_perspective_q[2] = q2; + if (g_perspective_valid) g_perspective_triangles++; +} + +uint32_t sw_perspective_triangle_count(void) { + return g_perspective_triangles; +} + +static inline int precise_scaled(int axis, int vertex, int fallback, int scale) { + if (!g_precise_valid) return fallback * scale; + int64_t fixed = axis ? g_precise_y16[vertex] : g_precise_x16[vertex]; + int64_t scaled = fixed * scale; + if (scaled >= 0) return (int)((scaled + 0x8000) >> 16); + return -(int)((-scaled + 0x8000) >> 16); +} + +static inline int precise_wide_x(int vertex, int fallback, int scale, + int dx, const WideBd *bd) { + if (g_precise_valid) + return precise_scaled(0, vertex, fallback, scale) + dx * scale; + return (wide_bd_x(bd, fallback) + dx) * scale; +} + +static inline void precise_consumed(void) { + g_precise_valid = 0; + g_perspective_valid = 0; +} + void sw_set_texture_filter(int bilinear) { g_texture_filter = bilinear ? 1 : 0; } int sw_texture_filter(void) { return g_texture_filter; } @@ -687,14 +735,21 @@ void sw_draw_flat_triangle(int x0, int y0, int x1, int y1, if (g_hr) { int s = g_scale; RTarget hr = rt_hires(); - raster_flat_triangle(&hr, x0*s, y0*s, x1*s, y1*s, x2*s, y2*s, color); + raster_flat_triangle(&hr, + precise_scaled(0,0,x0,s), precise_scaled(1,0,y0,s), + precise_scaled(0,1,x1,s), precise_scaled(1,1,y1,s), + precise_scaled(0,2,x2,s), precise_scaled(1,2,y2,s), color); } if (g_wide_cur) { int s = g_scale, dx = wide_dx(); RTarget wt = rt_wide(); WideBd bd = wide_bd_get(); - raster_flat_triangle(&wt, (wide_bd_x(&bd,x0)+dx)*s, y0*s, (wide_bd_x(&bd,x1)+dx)*s, y1*s, (wide_bd_x(&bd,x2)+dx)*s, y2*s, color); + raster_flat_triangle(&wt, + precise_wide_x(0,x0,s,dx,&bd), precise_scaled(1,0,y0,s), + precise_wide_x(1,x1,s,dx,&bd), precise_scaled(1,1,y1,s), + precise_wide_x(2,x2,s,dx,&bd), precise_scaled(1,2,y2,s), color); } + precise_consumed(); } /* ------------------------------------------------------------------ */ @@ -801,14 +856,21 @@ void sw_draw_gouraud_triangle(int x0, int y0, uint16_t c0, if (g_hr) { int s = g_scale; RTarget hr = rt_hires(); - raster_gouraud_triangle(&hr, x0*s, y0*s, c0, x1*s, y1*s, c1, x2*s, y2*s, c2); + raster_gouraud_triangle(&hr, + precise_scaled(0,0,x0,s), precise_scaled(1,0,y0,s), c0, + precise_scaled(0,1,x1,s), precise_scaled(1,1,y1,s), c1, + precise_scaled(0,2,x2,s), precise_scaled(1,2,y2,s), c2); } if (g_wide_cur) { int s = g_scale, dx = wide_dx(); RTarget wt = rt_wide(); WideBd bd = wide_bd_get(); - raster_gouraud_triangle(&wt, (wide_bd_x(&bd,x0)+dx)*s, y0*s, c0, (wide_bd_x(&bd,x1)+dx)*s, y1*s, c1, (wide_bd_x(&bd,x2)+dx)*s, y2*s, c2); + raster_gouraud_triangle(&wt, + precise_wide_x(0,x0,s,dx,&bd), precise_scaled(1,0,y0,s), c0, + precise_wide_x(1,x1,s,dx,&bd), precise_scaled(1,1,y1,s), c1, + precise_wide_x(2,x2,s,dx,&bd), precise_scaled(1,2,y2,s), c2); } + precise_consumed(); } /* ------------------------------------------------------------------ */ @@ -820,22 +882,27 @@ static void raster_textured_triangle(const RTarget *t, int x1, int y1, int u1, int v1, int x2, int y2, int u2, int v2, uint16_t clut_x, uint16_t clut_y, - uint16_t texpage) { + uint16_t texpage, + int perspective, + float q0, float q1, float q2) { /* Sort by Y, keeping UV in sync */ if (y0 > y1) { int tt; tt=x0; x0=x1; x1=tt; tt=y0; y0=y1; y1=tt; tt=u0; u0=u1; u1=tt; tt=v0; v0=v1; v1=tt; + float tq=q0; q0=q1; q1=tq; } if (y0 > y2) { int tt; tt=x0; x0=x2; x2=tt; tt=y0; y0=y2; y2=tt; tt=u0; u0=u2; u2=tt; tt=v0; v0=v2; v2=tt; + float tq=q0; q0=q2; q2=tq; } if (y1 > y2) { int tt; tt=x1; x1=x2; x2=tt; tt=y1; y1=y2; y2=tt; tt=u1; u1=u2; u2=tt; tt=v1; v1=v2; v2=tt; + float tq=q1; q1=q2; q2=tq; } int dy_total = y2 - y0; @@ -860,13 +927,23 @@ static void raster_textured_triangle(const RTarget *t, float ua = u0 + (float)(u2 - u0) * alpha; float va = v0 + (float)(v2 - v0) * alpha; + float qa = q0 + (q2 - q0) * alpha; + float uqa = (float)u0 * q0 + ((float)u2 * q2 - (float)u0 * q0) * alpha; + float vqa = (float)v0 * q0 + ((float)v2 * q2 - (float)v0 * q0) * alpha; float ub, vb; + float qb, uqb, vqb; if (second_half) { ub = u1 + (float)(u2 - u1) * beta; vb = v1 + (float)(v2 - v1) * beta; + qb = q1 + (q2 - q1) * beta; + uqb = (float)u1 * q1 + ((float)u2 * q2 - (float)u1 * q1) * beta; + vqb = (float)v1 * q1 + ((float)v2 * q2 - (float)v1 * q1) * beta; } else { ub = u0 + (float)(u1 - u0) * beta; vb = v0 + (float)(v1 - v0) * beta; + qb = q0 + (q1 - q0) * beta; + uqb = (float)u0 * q0 + ((float)u1 * q1 - (float)u0 * q0) * beta; + vqb = (float)v0 * q0 + ((float)v1 * q1 - (float)v0 * q0) * beta; } if (xa > xb) { @@ -874,6 +951,9 @@ static void raster_textured_triangle(const RTarget *t, float tf; tf = ua; ua = ub; ub = tf; tf = va; va = vb; vb = tf; + tf = qa; qa = qb; qb = tf; + tf = uqa; uqa = uqb; uqb = tf; + tf = vqa; vqa = vqb; vqb = tf; } int span = xb - xa; @@ -886,6 +966,13 @@ static void raster_textured_triangle(const RTarget *t, float t_val = (float)(x - xa) / (float)span; float fu = ua + (ub - ua) * t_val; float fv = va + (vb - va) * t_val; + if (perspective) { + float q = qa + (qb - qa) * t_val; + if (q > 1.0e-12f) { + fu = (uqa + (uqb - uqa) * t_val) / q; + fv = (vqa + (vqb - vqa) * t_val) / q; + } + } uint16_t texel = g_texture_filter ? texel_fetch_bilinear(fu, fv, texpage, clut_x, clut_y) @@ -902,7 +989,7 @@ void sw_draw_textured_triangle(int x0, int y0, int u0, int v0, uint16_t texpage) { int64_t area2 = (int64_t)(x1 - x0) * (int64_t)(y2 - y0) - (int64_t)(x2 - x0) * (int64_t)(y1 - y0); - if (area2 == 0) return; + if (area2 == 0) { precise_consumed(); return; } if (g_texture_filter) { int xs[3] = {x0,x1,x2}, ys[3] = {y0,y1,y2}; @@ -912,22 +999,31 @@ void sw_draw_textured_triangle(int x0, int y0, int u0, int v0, RTarget n = rt_native(); raster_textured_triangle(&n, x0, y0, u0, v0, x1, y1, u1, v1, - x2, y2, u2, v2, clut_x, clut_y, texpage); + x2, y2, u2, v2, clut_x, clut_y, texpage, + g_perspective_valid, + g_perspective_q[0], g_perspective_q[1], g_perspective_q[2]); if (g_hr) { int s = g_scale; RTarget hr = rt_hires(); - raster_textured_triangle(&hr, x0*s, y0*s, u0, v0, - x1*s, y1*s, u1, v1, - x2*s, y2*s, u2, v2, clut_x, clut_y, texpage); + raster_textured_triangle(&hr, + precise_scaled(0,0,x0,s), precise_scaled(1,0,y0,s), u0, v0, + precise_scaled(0,1,x1,s), precise_scaled(1,1,y1,s), u1, v1, + precise_scaled(0,2,x2,s), precise_scaled(1,2,y2,s), u2, v2, + clut_x, clut_y, texpage, g_perspective_valid, + g_perspective_q[0], g_perspective_q[1], g_perspective_q[2]); } if (g_wide_cur) { int s = g_scale, dx = wide_dx(); RTarget wt = rt_wide(); WideBd bd = wide_bd_get(); - raster_textured_triangle(&wt, (wide_bd_x(&bd,x0)+dx)*s, y0*s, u0, v0, - (wide_bd_x(&bd,x1)+dx)*s, y1*s, u1, v1, - (wide_bd_x(&bd,x2)+dx)*s, y2*s, u2, v2, clut_x, clut_y, texpage); + raster_textured_triangle(&wt, + precise_wide_x(0,x0,s,dx,&bd), precise_scaled(1,0,y0,s), u0, v0, + precise_wide_x(1,x1,s,dx,&bd), precise_scaled(1,1,y1,s), u1, v1, + precise_wide_x(2,x2,s,dx,&bd), precise_scaled(1,2,y2,s), u2, v2, + clut_x, clut_y, texpage, g_perspective_valid, + g_perspective_q[0], g_perspective_q[1], g_perspective_q[2]); } + precise_consumed(); } static inline void color24_to_mod(uint32_t color, int *r, int *g, int *b) { @@ -944,25 +1040,30 @@ static void raster_shaded_textured_triangle(const RTarget *t, int x2, int y2, int u2, int v2, int r2, int g2, int b2, uint16_t clut_x, uint16_t clut_y, - uint16_t texpage, int raw_texture) { + uint16_t texpage, int raw_texture, + int perspective, + float q0, float q1, float q2) { /* Sort by Y, keeping UV and color modulation in sync */ if (y0 > y1) { int tt; tt=x0; x0=x1; x1=tt; tt=y0; y0=y1; y1=tt; tt=u0; u0=u1; u1=tt; tt=v0; v0=v1; v1=tt; tt=r0; r0=r1; r1=tt; tt=g0; g0=g1; g1=tt; tt=b0; b0=b1; b1=tt; + float tq=q0; q0=q1; q1=tq; } if (y0 > y2) { int tt; tt=x0; x0=x2; x2=tt; tt=y0; y0=y2; y2=tt; tt=u0; u0=u2; u2=tt; tt=v0; v0=v2; v2=tt; tt=r0; r0=r2; r2=tt; tt=g0; g0=g2; g2=tt; tt=b0; b0=b2; b2=tt; + float tq=q0; q0=q2; q2=tq; } if (y1 > y2) { int tt; tt=x1; x1=x2; x2=tt; tt=y1; y1=y2; y2=tt; tt=u1; u1=u2; u2=tt; tt=v1; v1=v2; v2=tt; tt=r1; r1=r2; r2=tt; tt=g1; g1=g2; g2=tt; tt=b1; b1=b2; b2=tt; + float tq=q1; q1=q2; q2=tq; } int dy_total = y2 - y0; @@ -987,20 +1088,29 @@ static void raster_shaded_textured_triangle(const RTarget *t, float ua = u0 + (float)(u2 - u0) * alpha; float va = v0 + (float)(v2 - v0) * alpha; + float qa = q0 + (q2 - q0) * alpha; + float uqa = (float)u0 * q0 + ((float)u2 * q2 - (float)u0 * q0) * alpha; + float vqa = (float)v0 * q0 + ((float)v2 * q2 - (float)v0 * q0) * alpha; float ra = r0 + (float)(r2 - r0) * alpha; float ga = g0 + (float)(g2 - g0) * alpha; float ba = b0 + (float)(b2 - b0) * alpha; - float ub, vb, rb, gb, bb; + float ub, vb, qb, uqb, vqb, rb, gb, bb; if (second_half) { ub = u1 + (float)(u2 - u1) * beta; vb = v1 + (float)(v2 - v1) * beta; + qb = q1 + (q2 - q1) * beta; + uqb = (float)u1 * q1 + ((float)u2 * q2 - (float)u1 * q1) * beta; + vqb = (float)v1 * q1 + ((float)v2 * q2 - (float)v1 * q1) * beta; rb = r1 + (float)(r2 - r1) * beta; gb = g1 + (float)(g2 - g1) * beta; bb = b1 + (float)(b2 - b1) * beta; } else { ub = u0 + (float)(u1 - u0) * beta; vb = v0 + (float)(v1 - v0) * beta; + qb = q0 + (q1 - q0) * beta; + uqb = (float)u0 * q0 + ((float)u1 * q1 - (float)u0 * q0) * beta; + vqb = (float)v0 * q0 + ((float)v1 * q1 - (float)v0 * q0) * beta; rb = r0 + (float)(r1 - r0) * beta; gb = g0 + (float)(g1 - g0) * beta; bb = b0 + (float)(b1 - b0) * beta; @@ -1011,6 +1121,9 @@ static void raster_shaded_textured_triangle(const RTarget *t, float tf; tf = ua; ua = ub; ub = tf; tf = va; va = vb; vb = tf; + tf = qa; qa = qb; qb = tf; + tf = uqa; uqa = uqb; uqb = tf; + tf = vqa; vqa = vqb; vqb = tf; tf = ra; ra = rb; rb = tf; tf = ga; ga = gb; gb = tf; tf = ba; ba = bb; bb = tf; @@ -1026,6 +1139,13 @@ static void raster_shaded_textured_triangle(const RTarget *t, float t_val = (float)(x - xa) / (float)span; float fu = ua + (ub - ua) * t_val; float fv = va + (vb - va) * t_val; + if (perspective) { + float q = qa + (qb - qa) * t_val; + if (q > 1.0e-12f) { + fu = (uqa + (uqb - uqa) * t_val) / q; + fv = (vqa + (vqb - vqa) * t_val) / q; + } + } int mr = (int)(ra + (rb - ra) * t_val); int mg = (int)(ga + (gb - ga) * t_val); int mb = (int)(ba + (bb - ba) * t_val); @@ -1051,7 +1171,7 @@ void sw_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, uint16_t texpage, int raw_texture) { int64_t area2 = (int64_t)(x1 - x0) * (int64_t)(y2 - y0) - (int64_t)(x2 - x0) * (int64_t)(y1 - y0); - if (area2 == 0) return; + if (area2 == 0) { precise_consumed(); return; } if (g_texture_filter) { int xs[3] = {x0,x1,x2}, ys[3] = {y0,y1,y2}; @@ -1069,26 +1189,30 @@ void sw_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, x0, y0, u0, v0, r0, g0, b0, x1, y1, u1, v1, r1, g1, b1, x2, y2, u2, v2, r2, g2, b2, - clut_x, clut_y, texpage, raw_texture); + clut_x, clut_y, texpage, raw_texture, g_perspective_valid, + g_perspective_q[0], g_perspective_q[1], g_perspective_q[2]); if (g_hr) { int s = g_scale; RTarget hr = rt_hires(); raster_shaded_textured_triangle(&hr, - x0*s, y0*s, u0, v0, r0, g0, b0, - x1*s, y1*s, u1, v1, r1, g1, b1, - x2*s, y2*s, u2, v2, r2, g2, b2, - clut_x, clut_y, texpage, raw_texture); + precise_scaled(0,0,x0,s), precise_scaled(1,0,y0,s), u0, v0, r0, g0, b0, + precise_scaled(0,1,x1,s), precise_scaled(1,1,y1,s), u1, v1, r1, g1, b1, + precise_scaled(0,2,x2,s), precise_scaled(1,2,y2,s), u2, v2, r2, g2, b2, + clut_x, clut_y, texpage, raw_texture, g_perspective_valid, + g_perspective_q[0], g_perspective_q[1], g_perspective_q[2]); } if (g_wide_cur) { int s = g_scale, dx = wide_dx(); RTarget wt = rt_wide(); WideBd bd = wide_bd_get(); raster_shaded_textured_triangle(&wt, - (wide_bd_x(&bd,x0)+dx)*s, y0*s, u0, v0, r0, g0, b0, - (wide_bd_x(&bd,x1)+dx)*s, y1*s, u1, v1, r1, g1, b1, - (wide_bd_x(&bd,x2)+dx)*s, y2*s, u2, v2, r2, g2, b2, - clut_x, clut_y, texpage, raw_texture); + precise_wide_x(0,x0,s,dx,&bd), precise_scaled(1,0,y0,s), u0, v0, r0, g0, b0, + precise_wide_x(1,x1,s,dx,&bd), precise_scaled(1,1,y1,s), u1, v1, r1, g1, b1, + precise_wide_x(2,x2,s,dx,&bd), precise_scaled(1,2,y2,s), u2, v2, r2, g2, b2, + clut_x, clut_y, texpage, raw_texture, g_perspective_valid, + g_perspective_q[0], g_perspective_q[1], g_perspective_q[2]); } + precise_consumed(); } /* ------------------------------------------------------------------ */ diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index c2fd2253..b6471ac6 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -186,6 +186,144 @@ static int32_t s_ws_xnum = 1, s_ws_xden = 1; extern "C" int gpu_ws_present_native_43(void); /* gpu.c — suppress on 4:3 frames */ extern "C" void psx_ws_note_gte_project(int nverts); /* gpu.c — gte_game_mode stamp */ +/* Optional visual-only geometry precision cache. The PS1-visible SXY FIFO + * remains integer and fully faithful; this side cache retains the discarded + * 16.16 projection fraction so the high-resolution software mirror can match + * a later GP0 polygon and place its vertices between native pixels. */ +#define GEOM_CACHE_SIZE 8192u +struct GeomVertex { + uint32_t packed; + int32_t x16, y16; + uint32_t generation; +}; +static GeomVertex s_geom_cache[GEOM_CACHE_SIZE]; +static uint32_t s_geom_generation = 1; +static int s_geom_enabled = 0; +static uint32_t s_geom_hits = 0; + +/* Exact GTE projection provenance for perspective texture correction. The + * recompiler/interpreters call gte_precision_store after SWC2 writes an SXY + * register into guest RAM. GPU DMA later supplies that exact RAM address, so + * depth lookup follows the packet rather than guessing from rounded X/Y. */ +struct PreciseProjection { + uint32_t packed; + int32_t x16, y16; + uint16_t z; + uint8_t valid; +}; +static PreciseProjection s_precise_sxy[4] = {}; + +#define PRECISION_STORE_SIZE 65536u +struct PrecisionStoreEntry { + uint32_t addr; + PreciseProjection projection; + uint32_t generation; +}; +static PrecisionStoreEntry s_precision_store[PRECISION_STORE_SIZE]; +static uint32_t s_precision_generation = 1; +static int s_precision_tracking = 0; + +static inline uint32_t precision_hash(uint32_t addr) { + addr >>= 2; + addr ^= addr >> 11; + return (addr * 2654435761u) & (PRECISION_STORE_SIZE - 1u); +} + +static inline int precision_ram_address(uint32_t addr, uint32_t *physical) { + uint32_t mapped = addr & 0x1FFFFFFFu; + if (mapped >= 0x00800000u) return 0; + *physical = mapped & 0x1FFFFCu; + return 1; +} + +extern "C" void gte_precision_tracking_set(int enabled) { + s_precision_tracking = enabled ? 1 : 0; + s_precision_generation++; + if (!s_precision_generation) s_precision_generation = 1; +} + +extern "C" void gte_precision_store_word(uint32_t addr, uint8_t reg) { + if (!s_precision_tracking || reg < 12 || reg > 15) return; + int index = reg == 15 ? 2 : (int)reg - 12; + const PreciseProjection &projection = s_precise_sxy[index]; + if (!projection.valid) return; + uint32_t physical; + if (!precision_ram_address(addr, &physical)) return; + PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)]; + entry.addr = physical; + entry.projection = projection; + entry.generation = s_precision_generation; +} + +extern "C" void gte_precision_invalidate_word(uint32_t addr) { + if (!s_precision_tracking) return; + uint32_t physical; + if (!precision_ram_address(addr, &physical)) return; + PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)]; + if (entry.generation == s_precision_generation && entry.addr == physical) + entry.generation = 0; +} + +extern "C" int gte_precision_load_word(uint32_t addr, uint32_t packed, + int32_t *x16, int32_t *y16, + uint16_t *z) { + if (!s_precision_tracking) return 0; + uint32_t physical; + if (!precision_ram_address(addr, &physical)) return 0; + const PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)]; + if (entry.generation != s_precision_generation || entry.addr != physical || + !entry.projection.valid || entry.projection.packed != packed) + return 0; + if (x16) *x16 = entry.projection.x16; + if (y16) *y16 = entry.projection.y16; + if (z) *z = entry.projection.z; + return entry.projection.z != 0; +} + +static inline uint32_t geom_hash(uint32_t packed) { + packed ^= packed >> 16; + return (packed * 2654435761u) & (GEOM_CACHE_SIZE - 1u); +} + +extern "C" void gte_geometry_correction_set(int enabled) { + s_geom_enabled = enabled ? 1 : 0; + s_geom_hits = 0; + s_geom_generation++; + if (!s_geom_generation) s_geom_generation = 1; +} + +extern "C" int gte_geometry_correction_enabled(void) { + return s_geom_enabled; +} + +extern "C" uint32_t gte_geometry_correction_hits(void) { + return s_geom_hits; +} + +extern "C" int gte_geometry_correction_lookup(uint32_t packed, + int32_t *x16, int32_t *y16) { + if (!s_geom_enabled) return 0; + const GeomVertex &entry = s_geom_cache[geom_hash(packed)]; + if (entry.generation != s_geom_generation || entry.packed != packed) return 0; + if (x16) *x16 = entry.x16; + if (y16) *y16 = entry.y16; + s_geom_hits++; + return 1; +} + +static inline void geom_note(uint32_t packed, int64_t x16, int64_t y16) { + if (!s_geom_enabled) return; + /* Saturated off-screen projections are unsuitable for subpixel recovery. */ + int32_t x = (int16_t)(packed & 0xFFFFu); + int32_t y = (int16_t)(packed >> 16); + if (x <= -0x400 || x >= 0x3FF || y <= -0x400 || y >= 0x3FF) return; + GeomVertex &entry = s_geom_cache[geom_hash(packed)]; + entry.packed = packed; + entry.x16 = (int32_t)x16; + entry.y16 = (int32_t)y16; + entry.generation = s_geom_generation; +} + // Per-draw suppression of the X-squash (8C far-backdrop). The far backdrop // (ocean/cloud/distant mountain) is a parallax layer that is conceptually at // infinity: squashing it toward centre leaves its geometry short of the @@ -651,9 +789,20 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0) { } } dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */ - int64_t sx = (gte->OFX + xterm) >> 16; - int64_t sy = (gte->OFY + (int64_t)gte->IR2 * h_div_sz) >> 16; + int64_t sx16 = gte->OFX + xterm; + int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz; + int64_t sx = sx16 >> 16; + int64_t sy = sy16 >> 16; gte->push_sxy(sx, sy); + s_precise_sxy[0] = s_precise_sxy[1]; + s_precise_sxy[1] = s_precise_sxy[2]; + s_precise_sxy[2].packed = (uint32_t)gte->SXY[2]; + s_precise_sxy[2].x16 = (int32_t)sx16; + s_precise_sxy[2].y16 = (int32_t)sy16; + s_precise_sxy[2].z = gte->SZ[3]; + s_precise_sxy[2].valid = gte->SZ[3] != 0; + s_precise_sxy[3] = s_precise_sxy[2]; + geom_note((uint32_t)gte->SXY[2], sx16, sy16); // Step 5: Depth cueing (MAC0/IR0) — only for last vertex of RTPT or RTPS if (setMac0) { @@ -1384,6 +1533,8 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { GTEState gte; gte_load_data(gte, cpu); gte_mtc2(>e, reg, val); + if (reg >= 12 && reg <= 15) + for (int i = 0; i < 4; i++) s_precise_sxy[i].valid = 0; for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(>e, i); for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, i); } diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 90cd974f..e97833ec 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -1308,6 +1308,7 @@ static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) { } static void psx_write_word_raw(uint32_t addr, uint32_t val); +extern void gte_precision_invalidate_word(uint32_t addr); void psx_write_word(uint32_t addr, uint32_t val) { extern void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); @@ -1320,6 +1321,7 @@ void psx_write_word(uint32_t addr, uint32_t val) { } static void psx_write_word_raw(uint32_t addr, uint32_t val) { g_guest_store_count++; + gte_precision_invalidate_word(addr); /* KSEG2 cache control — before physical translation. */ if (addr == 0xFFFE0130u) { cache_ctrl = val; return; } /* KSEG2 guard — see psx_read_word_raw. */ @@ -1474,6 +1476,7 @@ void psx_write_half(uint32_t addr, uint16_t val) { } static void psx_write_half_raw(uint32_t addr, uint16_t val) { g_guest_store_count++; + gte_precision_invalidate_word(addr); if (sr_ptr && (*sr_ptr & 0x10000u)) return; /* KSEG2 guard — see psx_read_word_raw. */ @@ -1705,6 +1708,7 @@ void psx_write_byte(uint32_t addr, uint8_t val) { } static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; + gte_precision_invalidate_word(addr); if (sr_ptr && (*sr_ptr & 0x10000u)) return; /* KSEG2 guard — see psx_read_word_raw. */ diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 857be681..500ae334 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -3487,7 +3487,7 @@ void psx_sljit_cop2(CPUState *cpu, uint32_t insn) { int32_t simm = (int32_t)(int16_t)(insn & 0xFFFFu); uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; if (op == 0x32) { gte_write_data(cpu, (uint8_t)rt, cpu->read_word(addr)); } /* LWC2 */ - else if (op == 0x3A) { cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt)); } /* SWC2 */ + else if (op == 0x3A) { cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt)); gte_precision_store_word(addr, (uint8_t)rt); } /* SWC2 */ } /* Unaligned load/store helper — mirrors dirty_ram_interp.c interp_lwl/lwr/swl/swr diff --git a/runtime/src/psx_interpreter.c b/runtime/src/psx_interpreter.c index 4456c741..72fcf266 100644 --- a/runtime/src/psx_interpreter.c +++ b/runtime/src/psx_interpreter.c @@ -500,6 +500,7 @@ static void exec_one(CPUState* cpu) { case 0x3A: { /* SWC2 */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); cpu->write_word(addr, gte_read_data(cpu, RT(insn))); + gte_precision_store_word(addr, RT(insn)); break; } From 6b069bedb8502b4b4b88d64f7f471fed1585b43c Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 14 Jul 2026 10:13:24 -0700 Subject: [PATCH 6/6] feat: park duplicate-frame presentation smoothing Adapted from https://github.com/mstan/psxrecomp/commit/ec889d6e5956fc4f34acfee41d87f30b24123b7f Co-authored-by: Kareem Olim --- docs/internal/upstream/kem0x-pr14-smooth60.md | 17 +++ runtime/src/main.cpp | 129 +++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 docs/internal/upstream/kem0x-pr14-smooth60.md diff --git a/docs/internal/upstream/kem0x-pr14-smooth60.md b/docs/internal/upstream/kem0x-pr14-smooth60.md new file mode 100644 index 00000000..ca01bf97 --- /dev/null +++ b/docs/internal/upstream/kem0x-pr14-smooth60.md @@ -0,0 +1,17 @@ +# PR #14 duplicate-frame presentation smoothing + +Source: [kem0x PR #14](https://github.com/mstan/psxrecomp/pull/14), commit +[`ec889d6e5956fc4f34acfee41d87f30b24123b7f`](https://github.com/mstan/psxrecomp/commit/ec889d6e5956fc4f34acfee41d87f30b24123b7f). + +This branch generalizes the source's presentation-only 30-to-60 Hz experiment: +when software output repeats an exact guest image for two vblanks, the next +distinct image is presented first as a temporal midpoint. A scene-cut heuristic +avoids blending unrelated images. Guest execution, SPU timing, and GPU-direct or +FMV paths remain untouched. It is off by default, exposed through a generic C +setter, a Web export, and `PSX_SMOOTH_60FPS`. + +The smoother is mutually exclusive with the existing software frame-blend +masker. It is distinct from master's GL high-refresh interpolation path. + +Excluded: every title name, address, state hook, and scripted action from the +source commit. Authorship is retained in the commit trailer. diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 680ee5f1..bf3ec41d 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -56,7 +56,11 @@ #include "launcher.h" #endif #include +#if defined(PSX_WEB) +#include +#endif #include +#include #include #include #include @@ -274,6 +278,120 @@ static PlayerInput g_players[2]; * (sized for the native 640x512 when supersampling is off). */ static uint32_t* sdl_pixel_buf = nullptr; +/* Presentation-only interpolation for software-rendered content that repeats + * each guest image for two vblanks. This never changes guest or audio timing. */ +static std::atomic g_smooth_60fps{0}; + +struct Smooth60State { + std::vector previous_source; + uint64_t source_hash = 0; + uint32_t width = 0; + uint32_t height = 0; + bool have_source = false; + bool previous_was_duplicate = false; + bool announced = false; +}; + +static Smooth60State g_smooth_60_state; + +static void smooth_60_reset(void) { + g_smooth_60_state.previous_source.clear(); + g_smooth_60_state.source_hash = 0; + g_smooth_60_state.width = 0; + g_smooth_60_state.height = 0; + g_smooth_60_state.have_source = false; + g_smooth_60_state.previous_was_duplicate = false; +} + +static uint64_t smooth_60_frame_hash(const uint32_t* pixels, size_t count) { + uint64_t hash = 1469598103934665603ull; + const size_t step = std::max(1, count / 4096u); + for (size_t i = 0; i < count; i += step) { + hash ^= pixels[i]; + hash *= 1099511628211ull; + } + hash ^= count; + return hash * 1099511628211ull; +} + +static bool smooth_60_scene_cut(const uint32_t* current, + const uint32_t* previous, + size_t count) { + const size_t step = std::max(1, count / 4096u); + uint64_t difference = 0; + size_t samples = 0; + size_t large_changes = 0; + for (size_t i = 0; i < count; i += step) { + const uint32_t a = current[i]; + const uint32_t b = previous[i]; + const unsigned dr = (unsigned)std::abs((int)((a >> 16) & 0xFFu) - + (int)((b >> 16) & 0xFFu)); + const unsigned dg = (unsigned)std::abs((int)((a >> 8) & 0xFFu) - + (int)((b >> 8) & 0xFFu)); + const unsigned db = (unsigned)std::abs((int)(a & 0xFFu) - + (int)(b & 0xFFu)); + const unsigned pixel_difference = dr + dg + db; + difference += pixel_difference; + if (pixel_difference > 320u) large_changes++; + samples++; + } + return samples && large_changes * 4u > samples * 3u && + difference > (uint64_t)samples * 360u; +} + +static void smooth_60_present(uint32_t* pixels, uint32_t width, uint32_t height, + bool eligible) { + if (!eligible || !pixels || !g_smooth_60fps.load(std::memory_order_acquire)) { + if (g_smooth_60_state.have_source) smooth_60_reset(); + return; + } + Smooth60State& state = g_smooth_60_state; + const size_t count = (size_t)width * height; + if (!count) { smooth_60_reset(); return; } + const uint64_t hash = smooth_60_frame_hash(pixels, count); + if (!state.have_source || state.width != width || state.height != height) { + state.previous_source.assign(pixels, pixels + count); + state.source_hash = hash; + state.width = width; + state.height = height; + state.have_source = true; + state.previous_was_duplicate = false; + return; + } + const bool duplicate = hash == state.source_hash && + std::memcmp(pixels, state.previous_source.data(), count * sizeof(uint32_t)) == 0; + if (duplicate) { state.previous_was_duplicate = true; return; } + const bool interpolate = state.previous_was_duplicate && + !smooth_60_scene_cut(pixels, state.previous_source.data(), count); + state.previous_was_duplicate = false; + state.source_hash = hash; + if (!interpolate) { + std::copy(pixels, pixels + count, state.previous_source.begin()); + return; + } + if (!state.announced) { + std::fprintf(stdout, + "psxrecomp: 60 FPS smoothing active (guest timing preserved)\n"); + state.announced = true; + } + for (size_t i = 0; i < count; i++) { + const uint32_t current = pixels[i]; + const uint32_t previous = state.previous_source[i]; + state.previous_source[i] = current; + pixels[i] = 0xFF000000u | + (((current & 0x00FEFEFEu) >> 1) + ((previous & 0x00FEFEFEu) >> 1)); + } +} + +extern "C" void psx_smooth_60fps_set(int enabled) { + g_smooth_60fps.store(enabled ? 1 : 0, std::memory_order_release); +} +#if defined(PSX_WEB) +extern "C" EMSCRIPTEN_KEEPALIVE void psx_web_set_smooth_60fps(int enabled) { + psx_smooth_60fps_set(enabled); +} +#endif + /* [video] options, resolved from the game config (defaults: native + AA). */ static int g_video_scale = 1; /* internal-resolution SSAA factor */ static bool g_video_aa = true; /* linear present filtering */ @@ -2343,6 +2461,7 @@ static void sdl_vblank_present(void) { GpuDisplayInfo di; gpu_get_display_info(&di); if (di.disabled || di.width == 0 || di.height == 0) { + smooth_60_present(nullptr, 0, 0, false); present_ring_commit(PRES_PATH_BLANK, (uint16_t)di.width, (uint16_t)di.height, 0); #ifndef PSX_SDL_NO_RENDER @@ -2506,6 +2625,11 @@ static void sdl_vblank_present(void) { } } + smooth_60_present(sdl_pixel_buf, + present_w * (uint32_t)active_scale, + h * (uint32_t)active_scale, + !g_gl_active && !g_vk_active && !di.depth24 && !fmv_frame); + /* Frame blending (CRT-persistence masker for 30fps double-buffered * content). Some games (e.g. Crash Bash menus/characters) leave a * dynamic object in only one of the two display buffers per 30fps cycle, @@ -2524,7 +2648,8 @@ static void sdl_vblank_present(void) { } const bool simple_sw = !g_gl_active && !g_vk_active && !wide_present && !di.depth24 && active_scale == 1; - if (blend_cfg && simple_sw) { + if (blend_cfg && simple_sw && + !g_smooth_60fps.load(std::memory_order_acquire)) { static uint32_t prev_buf[640 * 512]; static uint32_t prev_px = 0; const uint32_t npx = present_w * h; @@ -3168,6 +3293,8 @@ int main(int argc, char** argv) { * PSX_FRAME_INTERPOLATION=0/1; PSX_FRAME_INTERPOLATION_FPS=0|90+. */ if (const char *e = std::getenv("PSX_LOW_LATENCY_INPUT")) g_low_latency_input = atoi(e) ? 1 : 0; if (const char *e = std::getenv("PSX_VSYNC")) g_video_vsync = atoi(e); + if (const char *e = std::getenv("PSX_SMOOTH_60FPS")) + psx_smooth_60fps_set(atoi(e) ? 1 : 0); if (const char *e = std::getenv("PSX_FRAME_INTERPOLATION")) g_frame_interpolation = atoi(e) ? 1 : 0; if (const char *e = std::getenv("PSX_FRAME_INTERPOLATION_FPS")) {