diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index bb9e60e2..80be5bdc 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -42,6 +42,13 @@ struct CodeGenConfig { // proportion correction at GP0 submission. Empty = no hooks (default). std::set 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 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 diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 4abdca8c..352e6bda 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -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. diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index ba750a84..ef368322 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -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 ws_backdrop_x_sites; + std::vector 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>(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>(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, }; } diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index cfa5c9fa..fb938e61 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -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 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 ws_backdrop_unsquash_funcs; }; // UserSettings — the launcher-written, user-editable override layer. diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index 01ade332..8fd680f9 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -95,6 +95,7 @@ int main(int argc, char** argv) { std::set ws_tag_funcs; // [widescreen] sprite_tag_funcs std::set ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull] std::set ws_backdrop_x; // [widescreen.backdrop] x_sites + std::set 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/_annotations.csv // Silently skipped if the file doesn't exist. diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 89722e52..c46e713f 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -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); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 29de08a8..8fbe7f30 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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= den= 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=] 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 }, diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 0ecbd359..5bd354ef 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -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;