mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 10:38:46 -04:00
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.
This commit is contained in:
@@ -158,6 +158,12 @@ struct CodeGenConfig {
|
||||
// while widescreen reveals extra world. 4:3 evaluates the original compare.
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> 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<PSXRecompV4::WidescreenCullWidenSite> 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<PSXRecompV4::WidescreenAngleSite> ws_cull_angle_sites;
|
||||
|
||||
@@ -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){};"
|
||||
|
||||
@@ -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<int64_t>(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<int>(n);
|
||||
}
|
||||
@@ -2281,7 +2285,7 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
});
|
||||
if (v.contains("supersampling")) try_get([&]{
|
||||
const auto n = toml::find<int64_t>(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<int64_t>(v, "window_width");
|
||||
|
||||
@@ -212,6 +212,7 @@ int main(int argc, char** argv) {
|
||||
std::set<uint32_t> ws_cull_nclip_keep; // [widescreen.cull] nclip_keep_sites
|
||||
std::set<uint32_t> ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
||||
std::vector<PSXRecompV4::WidescreenCullWidenSite> ws_cull_widen;
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+27
-1
@@ -11089,6 +11089,20 @@ int main(int argc, char** argv) {
|
||||
addresses.data(), expected.data(), results.data(),
|
||||
(int)addresses.size());
|
||||
}
|
||||
{
|
||||
std::vector<uint32_t> 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<uint32_t> 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) {
|
||||
|
||||
Reference in New Issue
Block a user