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) {