widescreen(8C): far-backdrop GTE un-squash, depth-gated

The far backdrop (ocean/cloud/distant mountain) is GTE-projected 3D drawn
by the main-EXE driver FUN_8004db3c (sole caller of the GTE emitter
FUN_80027600). The global widescreen GTE X-squash compresses it toward
centre, so its geometry falls short of the revealed 16:9 edges -> blue void
that pops as the camera moves.

Fix: bracket the backdrop driver with gte_ws_set_suppress(1)/(0) so its
draws skip the X-squash and fill the stretched frame (skybox treatment),
while the near 3D world keeps the wider FOV.

FUN_8004db3c draws a MIX (far backdrop + nearer props/scenery), so the
suppress is DEPTH-GATED in gte_rtps_internal: only vertices with projected
SZ >= s_ws_far_threshold (default 900, tunable live via the ws_far_threshold
TCP cmd) un-squash; nearer props stay squashed and aligned. Validated:
threshold 900 fills the void, keeps the foreground door + background
scenery (path/tree) aligned. Residual: the mountain grass-top straddles the
depth band (per-vertex limitation) -> flickers when the camera dips; this
is the checkpoint before exploring native-wide rendering to remove the
squash (and this whole artifact class) entirely.

Mechanism:
- recompiler: [widescreen.backdrop] unsquash_funcs emits
  gte_ws_set_suppress(1) at entry + (0) before each jr-$ra return
  (code_generator + config_loader + main_psx). Main-EXE -> regen-class.
- runtime: gte.cpp depth-gated squash + s_ws_far_threshold; cpu_state.h
  decl; debug_server.c ws_aspect (live squash on/off) + ws_far_threshold
  (set split + read SZ stats) diagnostics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-13 21:31:27 -07:00
parent 8ae73c610a
commit 1e28a9455a
8 changed files with 111 additions and 5 deletions
+7
View File
@@ -42,6 +42,13 @@ struct CodeGenConfig {
// proportion correction at GP0 submission. Empty = no hooks (default).
std::set<uint32_t> ws_sprite_tag_funcs;
// Widescreen far-backdrop un-squash ([widescreen] backdrop_unsquash_funcs):
// functions whose body is bracketed by gte_ws_set_suppress(1)/(0) so the
// GTE X-squash is OFF for their (far-backdrop) draws — the backdrop fills
// the stretched 16:9 frame instead of leaving edge void (8C). Suppress is
// set at entry and cleared before every jr-$ra return. Empty = none.
std::set<uint32_t> ws_backdrop_unsquash_funcs;
// Widescreen cull-margin widening ([widescreen] cull_* sites). At these
// addresses the immediate is emitted with a runtime margin term
// psx_ws_x_margin() so the world-space draw cull widens with the aspect
+10 -1
View File
@@ -1118,6 +1118,10 @@ std::string CodeGenerator::translate_basic_block(
}
if (ra_loaded_from_non_sp) break;
}
if (config_.ws_backdrop_unsquash_funcs.count(cfg.function_start)) {
ss << config_.indent
<< "gte_ws_set_suppress(0); /* widescreen: end far-backdrop un-squash (8C) */\n";
}
if (ra_loaded_from_non_sp) {
ss << config_.indent
<< "cpu->pc = cpu->gpr[31]; psx_restore_state_escape(); return;"
@@ -1363,6 +1367,10 @@ GeneratedFunction CodeGenerator::generate_function(
body_ss << config_.indent
<< "psx_ws_sprite_tag(cpu); /* widescreen: record prim ($a0) + anchor */\n";
}
if (config_.ws_backdrop_unsquash_funcs.count(func.start_addr)) {
body_ss << config_.indent
<< "gte_ws_set_suppress(1); /* widescreen: un-squash far backdrop (8C) */\n";
}
// Add function comment
if (config_.emit_comments) {
@@ -1692,7 +1700,8 @@ std::string CodeGenerator::generate_file(
ss << "extern void debug_server_log_call_entry(uint32_t func_addr);\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n\n";
ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n";
ss << "extern void gte_ws_set_suppress(int on); /* widescreen far-backdrop un-squash (gte.cpp) */\n\n";
// Emit reference implementations for unaligned memory helpers.
// These implement the MIPS lwl/lwr/swl/swr semantics.
+8 -1
View File
@@ -511,8 +511,10 @@ GameConfig load_game_config(const fs::path& config_path_in) {
}
}
// Optional [widescreen.backdrop] block — parallax 2D backdrop screenX squash.
// Optional [widescreen.backdrop] block — parallax 2D backdrop screenX squash
// (x_sites) + far-backdrop GTE un-squash (unsquash_funcs).
std::vector<uint32_t> ws_backdrop_x_sites;
std::vector<uint32_t> ws_backdrop_unsquash_funcs;
if (cfg.contains("widescreen")) {
const toml::value& ws = toml::find(cfg, "widescreen");
if (ws.contains("backdrop")) {
@@ -521,6 +523,10 @@ GameConfig load_game_config(const fs::path& config_path_in) {
for (const auto& a : toml::find<std::vector<std::string>>(bd, "x_sites"))
ws_backdrop_x_sites.push_back(
parse_hex(a, "widescreen.backdrop.x_sites"));
if (bd.contains("unsquash_funcs"))
for (const auto& a : toml::find<std::vector<std::string>>(bd, "unsquash_funcs"))
ws_backdrop_unsquash_funcs.push_back(
parse_hex(a, "widescreen.backdrop.unsquash_funcs"));
}
}
@@ -551,6 +557,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_cull_range_sites*/ ws_cull_range_sites,
/*ws_cull_a1_sites*/ ws_cull_a1_sites,
/*ws_backdrop_x_sites*/ ws_backdrop_x_sites,
/*ws_backdrop_unsquash_funcs*/ ws_backdrop_unsquash_funcs,
};
}
+6
View File
@@ -251,6 +251,12 @@ struct GameConfig {
// code, so the overlay compile must see this config (--ws-config). A regen
// is required; empty by default.
std::vector<uint32_t> ws_backdrop_x_sites;
// [widescreen.backdrop] unsquash_funcs — far-backdrop driver functions whose
// body is bracketed with gte_ws_set_suppress(1)/(0) so the GTE X-squash is
// OFF for their (far, parallax) draws: the backdrop fills the stretched 16:9
// frame instead of leaving edge void (8C). Main-EXE addresses; regen-class.
std::vector<uint32_t> ws_backdrop_unsquash_funcs;
};
// UserSettings — the launcher-written, user-editable override layer.
+6 -2
View File
@@ -95,6 +95,7 @@ int main(int argc, char** argv) {
std::set<uint32_t> ws_tag_funcs; // [widescreen] sprite_tag_funcs
std::set<uint32_t> ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull]
std::set<uint32_t> ws_backdrop_x; // [widescreen.backdrop] x_sites
std::set<uint32_t> ws_backdrop_unsquash; // [widescreen.backdrop] unsquash_funcs
std::filesystem::path out_dir = "generated";
if (!config_path.empty()) {
@@ -109,6 +110,7 @@ int main(int argc, char** argv) {
ws_cull_range.insert(cfg.ws_cull_range_sites.begin(), cfg.ws_cull_range_sites.end());
ws_cull_a1.insert(cfg.ws_cull_a1_sites.begin(), cfg.ws_cull_a1_sites.end());
ws_backdrop_x.insert(cfg.ws_backdrop_x_sites.begin(), cfg.ws_backdrop_x_sites.end());
ws_backdrop_unsquash.insert(cfg.ws_backdrop_unsquash_funcs.begin(), cfg.ws_backdrop_unsquash_funcs.end());
fmt::print("config: {}\n", config_path.string());
fmt::print(" exe = {}\n", exe_path.string());
fmt::print(" seeds = {}\n", extra_funcs_storage);
@@ -160,8 +162,9 @@ int main(int argc, char** argv) {
ws_cull_range.insert(wscfg.ws_cull_range_sites.begin(), wscfg.ws_cull_range_sites.end());
ws_cull_a1.insert(wscfg.ws_cull_a1_sites.begin(), wscfg.ws_cull_a1_sites.end());
ws_backdrop_x.insert(wscfg.ws_backdrop_x_sites.begin(), wscfg.ws_backdrop_x_sites.end());
fmt::print("ws-config: {} (backdrop_x sites={})\n",
ws_config_path.string(), ws_backdrop_x.size());
ws_backdrop_unsquash.insert(wscfg.ws_backdrop_unsquash_funcs.begin(), wscfg.ws_backdrop_unsquash_funcs.end());
fmt::print("ws-config: {} (backdrop_x sites={}, unsquash funcs={})\n",
ws_config_path.string(), ws_backdrop_x.size(), ws_backdrop_unsquash.size());
}
// Parse the PS1-EXE file
@@ -657,6 +660,7 @@ int main(int argc, char** argv) {
codegen_config.ws_cull_range_sites = ws_cull_range;
codegen_config.ws_cull_a1_sites = ws_cull_a1;
codegen_config.ws_backdrop_x_sites = ws_backdrop_x;
codegen_config.ws_backdrop_unsquash_funcs = ws_backdrop_unsquash;
// Load per-game annotations: annotations/<exe_stem>_annotations.csv
// Silently skipped if the file doesn't exist.
+1
View File
@@ -54,6 +54,7 @@ extern void gte_execute(CPUState* cpu, uint32_t cmd);
* RTPS/RTPT screen-X around the game's OFX so a 4:3 frame stretched to the
* wide aspect shows a wider field of view. */
extern void gte_set_display_aspect(int num, int den);
extern void gte_ws_set_suppress(int on); /* 8C: un-squash far backdrop draws */
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);
+32
View File
@@ -5206,6 +5206,36 @@ static void handle_ws_margin(int id, const char *json)
id, v, ws.x_margin, ws.active);
}
/* Live GTE widescreen-squash toggle (diagnostic for 8C far-backdrop void):
* ws_aspect num=<n> den=<d> calls gte_set_display_aspect at runtime so we can
* compare squash ON (e.g. 16/9) vs OFF (1/1) in-place without a relaunch. */
extern void gte_set_display_aspect(int num, int den);
static void handle_ws_aspect(int id, const char *json)
{
int num = json_get_int(json, "num", -1);
int den = json_get_int(json, "den", -1);
if (num <= 0 || den <= 0) { send_err(id, "need num>0 den>0 (1 1 = squash off)"); return; }
gte_set_display_aspect(num, den);
send_fmt("{\"id\":%d,\"ok\":true,\"num\":%d,\"den\":%d}", id, num, den);
}
/* 8C far-backdrop depth split. ws_far_threshold [t=<SZ>] sets the SZ cutoff
* above which backdrop-driver geometry is un-squashed (near props stay
* squashed). With no t=, just reports the observed SZ stats since last read so
* the threshold can be set from data. */
extern void gte_ws_set_far_threshold(int t);
extern int gte_ws_get_far_threshold(void);
extern void gte_ws_get_sz_stats(int* mn, int* mx, unsigned* n, unsigned* far_n);
static void handle_ws_far_threshold(int id, const char *json)
{
int t = json_get_int(json, "t", -123456789); /* sentinel = no change */
if (t != -123456789) gte_ws_set_far_threshold(t);
int mn = 0, mx = 0; unsigned n = 0, far_n = 0;
gte_ws_get_sz_stats(&mn, &mx, &n, &far_n);
send_fmt("{\"id\":%d,\"ok\":true,\"threshold\":%d,\"sz_min\":%d,\"sz_max\":%d,\"sz_n\":%u,\"sz_far\":%u}",
id, gte_ws_get_far_threshold(), mn, mx, n, far_n);
}
static void handle_ws_census(int id, const char *json)
{
char act[16] = {0};
@@ -8473,6 +8503,8 @@ static const CmdEntry s_commands[] = {
{ "write_ram", handle_write_ram },
{ "gpu_state", handle_gpu_state },
{ "ws_margin", handle_ws_margin },
{ "ws_aspect", handle_ws_aspect },
{ "ws_far_threshold", handle_ws_far_threshold },
{ "ws_census", handle_ws_census },
{ "mem_words", handle_mem_words },
{ "vram_peek", handle_vram_peek },
+41 -1
View File
@@ -121,6 +121,36 @@ static void depth_cue_from_ir(GTEState* gte) {
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 */
// 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
// revealed 16:9 edges (blue void). Drawing it UN-squashed lets its 4:3-extent
// fill the stretched frame (skybox treatment) while the near 3D world keeps the
// wider FOV. The recompiler brackets the backdrop driver (FUN_8004db3c) with
// gte_ws_set_suppress(1)/(0) ([widescreen] backdrop_unsquash_funcs).
// The driver (FUN_8004db3c) actually draws a MIX: the far backdrop (which we
// want un-squashed) AND nearer props (doors/mansion models) that must stay
// squashed or they drift relative to the rest of the world. So suppression is
// DEPTH-GATED: only vertices with SZ (projected Z) >= s_ws_far_threshold get
// un-squashed while suppress is active; nearer props keep the squash. Threshold
// tuned live via the ws_far_threshold TCP cmd; SZ stats below characterize the
// driver's depth range so the split is set from data, not a guess.
static int s_ws_suppress = 0;
static int32_t s_ws_far_threshold = 900; /* SZ split: near props squashed, far backdrop un-squashed (8C, tunable via ws_far_threshold) */
extern "C" void gte_ws_set_suppress(int on) { s_ws_suppress = on ? 1 : 0; }
extern "C" void gte_ws_set_far_threshold(int t) { s_ws_far_threshold = t; }
extern "C" int gte_ws_get_far_threshold(void) { return (int)s_ws_far_threshold; }
/* SZ stats observed while suppress is active (one frame window, reset on read). */
static int32_t s_ws_sz_min = 0x7FFFFFFF, s_ws_sz_max = 0;
static uint32_t s_ws_sz_n = 0, s_ws_sz_far = 0;
extern "C" void gte_ws_get_sz_stats(int* mn, int* mx, unsigned* n, unsigned* far_n) {
if (mn) *mn = (s_ws_sz_n ? s_ws_sz_min : 0);
if (mx) *mx = s_ws_sz_max;
if (n) *n = s_ws_sz_n;
if (far_n) *far_n = s_ws_sz_far;
s_ws_sz_min = 0x7FFFFFFF; s_ws_sz_max = 0; s_ws_sz_n = 0; s_ws_sz_far = 0;
}
extern "C" void gte_set_display_aspect(int num, int den) {
if (num <= 0 || den <= 0) { s_ws_xnum = s_ws_xden = 1; return; }
// squash = (4/3) / (num/den) = (4*den) / (3*num); identity for 4:3.
@@ -170,7 +200,17 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0) {
// this frame is being stretched — never on a 4:3-presented frame (FMV /
// full-2D screen), so content and present stay locked.
int64_t xterm = (int64_t)gte->IR1 * h_div_sz;
if (s_ws_xnum != s_ws_xden && !gpu_ws_present_native_43())
bool do_squash = (s_ws_xnum != s_ws_xden) && !gpu_ws_present_native_43();
if (do_squash && s_ws_suppress) {
/* Depth-gated: un-squash only FAR geometry (the backdrop); keep near
* props squashed so they stay aligned. Record SZ stats for tuning. */
int32_t sz = gte->SZ[3];
if (sz < s_ws_sz_min) s_ws_sz_min = sz;
if (sz > s_ws_sz_max) s_ws_sz_max = sz;
s_ws_sz_n++;
if (sz >= s_ws_far_threshold) { do_squash = false; s_ws_sz_far++; }
}
if (do_squash)
xterm = xterm * s_ws_xnum / s_ws_xden;
int64_t sx = (gte->OFX + xterm) >> 16;
int64_t sy = (gte->OFY + (int64_t)gte->IR2 * h_div_sz) >> 16;