From fd625f09969e306df5ebaf531e36f656b856182b Mon Sep 17 00:00:00 2001 From: tetrisgm <30949000+tetrisgm@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:41:58 -0700 Subject: [PATCH] Interpreter parity for cull.widen; raise the GL supersampling ceiling Two follow-ups to [[widescreen.cull.widen]], both found by testing it. INTERPRETER PARITY. The widen site type was recompiler-only: the AOT image evaluated the widened compare while the dirty-RAM interpreter, and any overlay shard built without the config, still evaluated the vanilla one. The same site would then cull differently depending on which backend happened to run it. Adds a runtime site registry mirroring psx_ws_cull_keep_site, wires it from the config at startup, and consults it from the interpreter's SLT and SLTI paths before the keep lookup so a config migrated from keep to widen cannot be served the pinned answer by a stale entry. GL SUPERSAMPLING CEILING 4x -> 16x. SW_MAX_INTERNAL_SCALE was applied to every backend, which is the wrong constraint for GL. It exists because the software path allocates a VRAM-sized hi-res MIRROR costing 1 MB * scale^2 (256 MB at 16x); under GL that mirror stays at 1x (glb_set_scale) and the cost is an FBO instead. Capping GL at 4x limited it to about 2048x960 internal on hardware that handles far more. GL_MAX_INTERNAL_SCALE moves to gpu_render.h (main.cpp needs it to pick the ceiling before a context exists), main.cpp selects the ceiling per backend, and the config range becomes 1..16. init_gpu_raster now also clamps to the driver's real GL_MAX_TEXTURE_SIZE / GL_MAX_RENDERBUFFER_SIZE and says so: previously an over-large scale reached glTexImage2D, failed silently, and surfaced only as "GL FBO incomplete" with no hint that the scale caused it. Measured on WipEout 3 SE (PAL, NVIDIA, Release build + debug tools), sustained present rate in-race, same track: scale internal fps 1 508x256 50.0 6 3048x1536 49.8 9 4572x2304 50.2 16 8128x4096 50.0 Flat across the range - the title is guest-bound, so 8K internal is free on this hardware. 50 fps is full speed for a PAL disc. --- recompiler/include/code_generator.h | 6 ++++ recompiler/src/code_generator.cpp | 8 ++--- recompiler/src/config_loader.cpp | 10 ++++-- recompiler/src/main_psx.cpp | 4 +++ runtime/include/gpu.h | 7 ++++ runtime/include/gpu_render.h | 9 +++++ runtime/src/dirty_ram_interp.c | 12 ++++++- runtime/src/gpu.c | 52 +++++++++++++++++++++++++++++ runtime/src/gpu_gl_renderer.c | 28 +++++++++++++++- runtime/src/main.cpp | 28 +++++++++++++++- 10 files changed, 154 insertions(+), 10 deletions(-) diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index 803a2687..ab643737 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -158,6 +158,12 @@ struct CodeGenConfig { // while widescreen reveals extra world. 4:3 evaluates the original compare. std::vector ws_cull_keep_sites; + // Exact, full-word-guarded comparison sites whose BOUND follows the live + // reveal margin. Unlike a keep site the verdict is never pinned, so a + // clip-code packer keeps classifying honestly and simply rejects at the + // revealed edge. Identity at 4:3 in every mode. + std::vector ws_cull_widen_sites; + // Exact `addi[u] rt,zero,imm` 12-bit angular half-extents. The runtime // scales tan(angle) by the current horizontal reveal factor. std::vector ws_cull_angle_sites; diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index f54833f7..1d5ee098 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -966,8 +966,8 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) site.expected, addr, instr); std::exit(1); } - const bool imm_mode = site.mode == WsCullWidenMode::ImmUpper || - site.mode == WsCullWidenMode::ImmLower; + const bool imm_mode = site.mode == PSXRecompV4::WsCullWidenMode::ImmUpper || + site.mode == PSXRecompV4::WsCullWidenMode::ImmLower; uint32_t dst = 0; std::string call; if (imm_mode) { @@ -980,7 +980,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) dst = get_rt(instr); call = fmt::format( "{}({}, {}u)", - site.mode == WsCullWidenMode::ImmUpper ? "psx_ws_cull_slti" + site.mode == PSXRecompV4::WsCullWidenMode::ImmUpper ? "psx_ws_cull_slti" : "psx_ws_cull_slti_lower", reg_name(get_rs(instr)), get_imm16(instr)); } else { @@ -994,7 +994,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) call = fmt::format( "psx_ws_cull_slt_widen({}, {}, {})", reg_name(get_rs(instr)), reg_name(get_rt(instr)), - site.mode == WsCullWidenMode::BoundRt ? 1 : 0); + site.mode == PSXRecompV4::WsCullWidenMode::BoundRt ? 1 : 0); } return fmt::format( "{} = (uint32_t){};" diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 97daa374..99101a25 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -557,9 +557,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& const toml::value& video = toml::find(cfg, "video"); if (video.contains("supersampling")) { const auto n = toml::find(video, "supersampling"); - if (n < 1 || n > 4) { + /* GL renders SSAA into an FBO rather than the software path's + * VRAM-sized mirror, so it scales well past 4x. The runtime picks + * the ceiling per backend and clamps again to the driver's real + * texture limit once a context exists. */ + if (n < 1 || n > 16) { throw std::runtime_error(fmt::format( - "[video] supersampling out of range (1..4): {}", n)); + "[video] supersampling out of range (1..16): {}", n)); } rt.video_supersampling = static_cast(n); } @@ -2281,7 +2285,7 @@ UserSettings load_user_settings(const fs::path& path) { }); if (v.contains("supersampling")) try_get([&]{ const auto n = toml::find(v, "supersampling"); - if (n >= 1 && n <= 4) { s.supersampling = (int)n; s.has_supersampling = true; } + if (n >= 1 && n <= 16) { s.supersampling = (int)n; s.has_supersampling = true; } }); if (v.contains("window_width")) try_get([&]{ const auto n = toml::find(v, "window_width"); diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index 243091c1..97e1ff63 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -212,6 +212,7 @@ int main(int argc, char** argv) { std::set ws_cull_nclip_keep; // [widescreen.cull] nclip_keep_sites std::set ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites std::vector ws_cull_keep; + std::vector ws_cull_widen; std::vector ws_cull_angle; PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone; int ws_cull_activation_guard_pixels = 0; @@ -275,6 +276,7 @@ int main(int argc, char** argv) { ws_cull_nclip_keep.insert(cfg.ws_cull_nclip_keep_sites.begin(), cfg.ws_cull_nclip_keep_sites.end()); ws_cull_branch_keep.insert(cfg.ws_cull_branch_keep_sites.begin(), cfg.ws_cull_branch_keep_sites.end()); ws_cull_keep = cfg.ws_cull_keep_sites; + ws_cull_widen = cfg.ws_cull_widen_sites; ws_cull_angle = cfg.ws_cull_angle_sites; ws_aspect_cone = cfg.ws_aspect_cone; ws_cull_activation_guard_pixels = @@ -370,6 +372,7 @@ int main(int argc, char** argv) { ws_cull_nclip_keep.insert(wscfg.ws_cull_nclip_keep_sites.begin(), wscfg.ws_cull_nclip_keep_sites.end()); ws_cull_branch_keep.insert(wscfg.ws_cull_branch_keep_sites.begin(), wscfg.ws_cull_branch_keep_sites.end()); if (ws_cull_keep.empty()) ws_cull_keep = wscfg.ws_cull_keep_sites; + if (ws_cull_widen.empty()) ws_cull_widen = wscfg.ws_cull_widen_sites; if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites; if (ws_aspect_cone.sites.empty()) ws_aspect_cone = wscfg.ws_aspect_cone; @@ -1234,6 +1237,7 @@ int main(int argc, char** argv) { codegen_config.ws_cull_nclip_keep_sites = ws_cull_nclip_keep; codegen_config.ws_cull_branch_keep_sites = ws_cull_branch_keep; codegen_config.ws_cull_keep_sites = ws_cull_keep; + codegen_config.ws_cull_widen_sites = ws_cull_widen; codegen_config.ws_cull_angle_sites = ws_cull_angle; codegen_config.ws_aspect_cone = ws_aspect_cone; codegen_config.ws_cull_w_imms = ws_cull_w_imms; diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 7b3281a0..0bd14025 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -260,6 +260,11 @@ void gpu_ws_set_xclip_load_sites(const uint32_t *sites, int nsites); void gpu_ws_set_cull_keep_sites(const uint32_t *addresses, const uint32_t *expected, const uint32_t *results, int nsites); +/* [[widescreen.cull.widen]] sites, so the dirty-RAM interpreter and any overlay + * shard reach the same widened verdict the AOT image does. */ +void gpu_ws_set_cull_widen_sites(const uint32_t *addresses, + const uint32_t *expected, + const uint32_t *modes, int nsites); void gpu_ws_set_angle_sites(const uint32_t *addresses, const uint32_t *expected, int nsites); void gpu_ws_set_aspect_cone(const uint32_t *addresses, @@ -293,6 +298,8 @@ uint32_t psx_ws_xclip_bound(uint32_t vanilla); uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced); int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla, uint32_t *out); +int psx_ws_cull_widen_site(uint32_t pc, uint32_t instr, uint32_t rs, + uint32_t rt, uint32_t imm, uint32_t *out); uint32_t psx_ws_angle_widen(uint32_t vanilla); int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out); uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla, diff --git a/runtime/include/gpu_render.h b/runtime/include/gpu_render.h index ace31f11..ab991508 100644 --- a/runtime/include/gpu_render.h +++ b/runtime/include/gpu_render.h @@ -201,6 +201,15 @@ typedef struct GpuRenderBackend { int base_x); } GpuRenderBackend; +/* GL supersampling ceiling. Deliberately separate from SW_MAX_INTERNAL_SCALE: + * the software path allocates a VRAM-sized hi-res MIRROR costing 1 MB * scale^2 + * (256 MB at 16x), but under GL that mirror stays at 1x and the cost is an FBO + * the GPU already has memory for. 16x is a 16384x8192 render target, the + * GL_MAX_TEXTURE_SIZE floor for desktop GL 3.3; the GL backend clamps again to + * the driver's real limit once a context exists. */ +#define GL_MAX_INTERNAL_SCALE 16 + + #ifdef __cplusplus } #endif diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 77c41310..641b3d73 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1755,7 +1755,13 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t vanilla = ((int32_t)cpu->gpr[rs] < (int32_t)cpu->gpr[rt]) ? 1u : 0u; uint32_t kept = vanilla; - if (!psx_ws_aspect_cone_site(cpu, pc, insn, vanilla, &kept)) + /* A widen site moves the bound; a keep site pins the verdict. A + * site is only ever one of the three, but check widen first so a + * config migrated from keep to widen cannot be served the pinned + * answer by a stale entry. */ + if (!psx_ws_aspect_cone_site(cpu, pc, insn, vanilla, &kept) && + !psx_ws_cull_widen_site(pc, insn, cpu->gpr[rs], cpu->gpr[rt], + 0u, &kept)) (void)psx_ws_cull_keep_site(pc, insn, vanilla, &kept); cpu->gpr[rd] = kept; cpu->gpr[0] = 0; @@ -1924,6 +1930,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, { uint32_t vanilla = ((int32_t)cpu->gpr[rs] < simm) ? 1u : 0u; uint32_t kept = vanilla; + uint32_t widened = 0; if (psx_ws_aspect_cone_site(cpu, pc, insn, vanilla, &kept)) cpu->gpr[rt] = kept; else if (psx_ws_cull_keep_site(pc, insn, vanilla, &kept)) @@ -1933,6 +1940,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, /* Widescreen render-funnel RIGHT-edge widen (auto_screen_x) for the * signed min/max funnel idiom (`slti v, minSX, W`) — the paired left * edge is the bltz above. Identity at 4:3 (margin 0). */ + else if (psx_ws_cull_widen_site(pc, insn, cpu->gpr[rs], 0u, imm, + &widened)) + cpu->gpr[rt] = widened; else if (psx_ws_is_cull_slti_lower_site(pc)) cpu->gpr[rt] = (uint32_t)psx_ws_cull_slti_lower( cpu->gpr[rs], imm); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 3c63ef31..08582536 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -630,6 +630,58 @@ int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla, return 0; } +/* [[widescreen.cull.widen]] site registry — the interpreter's half. + * + * The recompiler emits the widened helper directly into native code, but the + * same PC can also execute under the dirty-RAM interpreter, or inside an + * overlay shard built without this config. Without a runtime lookup those + * paths would evaluate the VANILLA compare while the AOT image evaluates the + * widened one, so the same site would cull differently depending on which + * backend happened to run it. Registering the sites here keeps both paths on + * the same verdict, exactly as the keep registry above does. */ +typedef struct { + uint32_t address; + uint32_t expected; + uint32_t mode; /* WsCullWidenMode: 0 imm_upper, 1 imm_lower, + * 2 bound_rt, 3 bound_rs */ +} WsCullWidenSite; +static WsCullWidenSite ws_cull_widen_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static int ws_cull_widen_n = 0; + +void gpu_ws_set_cull_widen_sites(const uint32_t *addresses, + const uint32_t *expected, + const uint32_t *modes, int nsites) { + if (nsites < 0) nsites = 0; + if (nsites > WS_EXPLICIT_CULL_SITES_MAX) nsites = WS_EXPLICIT_CULL_SITES_MAX; + ws_cull_widen_n = nsites; + for (int i = 0; i < nsites; i++) { + ws_cull_widen_sites[i].address = addresses[i] & 0x1FFFFFFFu; + ws_cull_widen_sites[i].expected = expected[i]; + ws_cull_widen_sites[i].mode = modes[i]; + } +} + +/* Returns 1 and writes the widened verdict when `pc`/`instr` is a registered + * widen site. `rs`/`rt` are the live operand values; `imm` the sign-extendable + * immediate for the SLTI forms. Identity at margin 0 in every mode. */ +int psx_ws_cull_widen_site(uint32_t pc, uint32_t instr, uint32_t rs, + uint32_t rt, uint32_t imm, uint32_t *out) { + const uint32_t phys = pc & 0x1FFFFFFFu; + for (int i = 0; i < ws_cull_widen_n; i++) { + const WsCullWidenSite *site = &ws_cull_widen_sites[i]; + if (site->address != phys || site->expected != instr) continue; + if (!out) return 1; + switch (site->mode) { + case 0: *out = (uint32_t)psx_ws_cull_slti(rs, imm); break; + case 1: *out = (uint32_t)psx_ws_cull_slti_lower(rs, imm); break; + case 2: *out = (uint32_t)psx_ws_cull_slt_widen(rs, rt, 1); break; + default: *out = (uint32_t)psx_ws_cull_slt_widen(rs, rt, 0); break; + } + return 1; + } + return 0; +} + typedef struct { uint32_t address; uint32_t expected; diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 8b34a195..9a62c0ac 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -126,7 +126,8 @@ #define VRAM_W 1024 #define VRAM_H 512 -#define GL_MAX_INTERNAL_SCALE 4 +/* GL_MAX_INTERNAL_SCALE lives in gpu_render.h — main.cpp needs it to pick the + * per-backend ceiling before the GL context exists. */ /* ---- Loaded modern-GL entry points ------------------------------------- */ typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum); @@ -2538,6 +2539,31 @@ static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) { } static int init_gpu_raster(void) { + /* Clamp the requested SSAA scale to what this driver can actually back. + * The hi-res target is VRAM_W x VRAM_H at `scale`, so the limiting factor + * is GL_MAX_TEXTURE_SIZE (and the renderbuffer limit for the depth/stencil + * attachment). Without this a too-large scale reaches glTexImage2D, fails + * silently, and surfaces only as "GL FBO incomplete" with no indication + * that the scale was the cause. */ + { + GLint max_tex = 0, max_rb = 0; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_tex); + glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &max_rb); + int lim = max_tex; + if (max_rb > 0 && max_rb < lim) lim = max_rb; + if (lim > 0) { + /* Width is the binding dimension: VRAM_W (1024) > VRAM_H (512). */ + int max_scale = lim / VRAM_W; + if (max_scale < 1) max_scale = 1; + if (s_req_scale > max_scale) { + fprintf(stdout, + "psxrecomp: GL supersampling %dx exceeds this driver's " + "%dx%d texture limit; clamping to %dx\n", + s_req_scale, lim, lim, max_scale); + s_req_scale = max_scale; + } + } + } s_scale = s_req_scale; s_geo_prog = build_program(GEO_VS, GEO_FS); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 501ef110..5847d717 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -11089,6 +11089,20 @@ int main(int argc, char** argv) { addresses.data(), expected.data(), results.data(), (int)addresses.size()); } + { + std::vector addresses, expected, modes; + addresses.reserve(gc.ws_cull_widen_sites.size()); + expected.reserve(gc.ws_cull_widen_sites.size()); + modes.reserve(gc.ws_cull_widen_sites.size()); + for (const auto& site : gc.ws_cull_widen_sites) { + addresses.push_back(site.address); + expected.push_back(site.expected); + modes.push_back((uint32_t)site.mode); + } + gpu_ws_set_cull_widen_sites( + addresses.data(), expected.data(), modes.data(), + (int)addresses.size()); + } { std::vector addresses, expected; addresses.reserve(gc.ws_cull_angle_sites.size()); @@ -12719,7 +12733,19 @@ session_reboot: * Dual-raster: gr_set_scale(N) arms GL hr FBO @ N× while glb_set_scale * keeps SW at 1×. SW-only netplay: force scale 1. Offline: full SSAA. */ if (g_video_scale < 1) g_video_scale = 1; - if (g_video_scale > SW_MAX_INTERNAL_SCALE) g_video_scale = SW_MAX_INTERNAL_SCALE; + /* The ceiling is per-backend, not global. SW_MAX_INTERNAL_SCALE exists + * because the software path allocates a VRAM-sized hi-res MIRROR that costs + * 1 MB * scale^2; under GL that mirror stays at 1x (glb_set_scale) and the + * cost is an FBO instead, so GL can go considerably higher. Applying the + * software limit to every backend capped GL at 4x -- about 2048x960 + * internal -- which is well short of 1440p/4K/8K on hardware that can + * trivially do it. The GL backend clamps again to the driver's real texture + * limit once the context exists. */ + { + const int max_scale = (g_video_renderer == 1) ? GL_MAX_INTERNAL_SCALE + : SW_MAX_INTERNAL_SCALE; + if (g_video_scale > max_scale) g_video_scale = max_scale; + } if (net_cfg.enabled && s_netplay_gl_present && gl_renderer_cpu_auth_dual()) { gr_set_scale(g_video_scale); if (g_video_scale > 1) {