From d9dae48c8e1144102f4e4dc449914df3b0d5b9fe Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 10 Jul 2026 00:17:17 -0700 Subject: [PATCH 1/2] widescreen: generalize Capcom 2D background hooks for MMX5 --- recompiler/include/code_generator.h | 3 +- recompiler/src/code_generator.cpp | 45 ++++++--- recompiler/src/config_loader.cpp | 37 +++++++ recompiler/src/config_loader.h | 19 +++- recompiler/src/main_psx.cpp | 4 + runtime/include/gpu.h | 12 ++- runtime/src/gpu.c | 148 +++++++++++++++++++--------- runtime/src/main.cpp | 9 ++ tools/regen_bios.sh | 1 + 9 files changed, 213 insertions(+), 65 deletions(-) diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index dafe0870..c526eb74 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -65,6 +65,7 @@ struct CodeGenConfig { std::set ws_cull_bias_sites; std::set ws_cull_range_sites; std::set ws_cull_a1_sites; + std::set ws_cull_screen_x_sites; // Widescreen automatic horizontal-FOV cull widening ([widescreen.cull] // auto_screen_x). When true, any function carrying the GTE screen-extent @@ -92,7 +93,7 @@ struct CodeGenConfig { // Widescreen pure-2D background tile-loop widen ([widescreen.bg2d]). Three // instruction addresses in a 2D game's per-layer BG renderer (MMX6's - // FUN_800270d0): the column-count load, the start tile-column mask, and the + // FUN_800270d0): the column-count load/compare, the start tile-column mask, and the // start screen-x shift, rewritten through the gpu.c psx_ws_mmx6_bg_* helpers // so the loop draws the 16:9 reveal columns on both sides (identity at 4:3 / // 512 hi-res). 0 = unset. Main-EXE addresses; verified by opcode at gen time. diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 50ede15d..a93b39fe 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -838,6 +838,20 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) } // overlay variant: addr is different code here — fall through to vanilla. } + if (config_.ws_cull_screen_x_sites.count(addr)) { + if (opcode == 0x0B) { // sltiu rt,rs,imm + uint32_t rs = get_rs(instr), rt = get_rt(instr); + uint16_t imm = get_imm16_u(instr); + return fmt::format("{} = psx_ws_cull_sltiu({}, {});" + " /* ws explicit screen-x cull */{}", + reg_name(rt), reg_name(rs), imm, comment); + } else if (!config_.overlay_mode) { + fmt::print(stderr, "ERROR: [widescreen.cull] screen_x site 0x{:08X} " + "is not sltiu (opcode 0x{:02X})\n", addr, opcode); + std::exit(1); + } + // Overlay variant: the configured main-EXE PC holds different code. + } // Widescreen automatic horizontal-FOV cull widening ([widescreen.cull] // auto_screen_x). ws_auto_cull_func_ is set when this function carries the // GTE screen-extent reject signature, so a per-vertex width compare here is @@ -884,7 +898,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) // overlay variant: addr is different code here — fall through to vanilla. } - // Widescreen pure-2D background tile-loop widen ([widescreen.bg2d]). MMX6's + // Widescreen pure-2D background tile-loop widen ([widescreen.bg2d]). The // per-layer BG renderer draws `count` 16px tile columns from a start column / // start screen-x derived from the camera scroll. Rewrite those three values // (via the gpu.c psx_ws_mmx6_bg_* helpers — identity at 4:3 / 512 hi-res) so @@ -894,11 +908,16 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) if (opcode == 0x09 || opcode == 0x0D) { // addiu / ori (li rt,imm) uint32_t rt = get_rt(instr); uint16_t imm = get_imm16_u(instr); - return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_cols({});{}", + return fmt::format("{} = (uint32_t)psx_ws_bg2d_cols({});{}", reg_name(rt), (int)imm, comment); + } else if (opcode == 0x0B) { // sltiu rt,rs,imm (loop bound compare) + uint32_t rs = get_rs(instr), rt = get_rt(instr); + uint16_t imm = get_imm16_u(instr); + return fmt::format("{} = ({} < (uint32_t)psx_ws_bg2d_cols({})) ? 1u : 0u;{}", + reg_name(rt), reg_name(rs), (int)imm, comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.bg2d] count_site 0x{:08X} is not " - "addiu/ori (opcode 0x{:02X})\n", addr, opcode); + "addiu/ori/sltiu (opcode 0x{:02X})\n", addr, opcode); std::exit(1); } } @@ -906,8 +925,8 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) if (opcode == 0x0C) { // andi rt,rs,imm (start tile-column mask) uint32_t rs = get_rs(instr), rt = get_rt(instr); uint16_t imm = get_imm16_u(instr); - return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_startcol((int)({} & 0x{:X}u));{}", - reg_name(rt), reg_name(rs), imm, comment); + return fmt::format("{} = (uint32_t)psx_ws_bg2d_startcol((int)({} & 0x{:X}u), 0x{:X}u);{}", + reg_name(rt), reg_name(rs), imm, imm, comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.bg2d] startcol_site 0x{:08X} is not " "andi (opcode 0x{:02X})\n", addr, opcode); @@ -917,7 +936,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) if (config_.ws_bg2d_startx_site && addr == config_.ws_bg2d_startx_site) { if (opcode == 0x00 && (instr & 0x3F) == 0x03) { // sra rd,rt,sa (start screen-x) uint32_t rt = get_rt(instr), rd = get_rd(instr), sh = get_shamt(instr); - return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_startx((int32_t){} >> {});{}", + return fmt::format("{} = (uint32_t)psx_ws_bg2d_startx((int32_t){} >> {});{}", reg_name(rd), reg_name(rt), sh, comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.bg2d] startx_site 0x{:08X} is not " @@ -932,7 +951,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) if (opcode == 0x08 || opcode == 0x09) { // addi / addiu uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t imm = get_imm16(instr); - return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_stream_left((int32_t){} + ({}));{}", + return fmt::format("{} = (uint32_t)psx_ws_bg2d_stream_left((int32_t){} + ({}));{}", reg_name(rt), reg_name(rs), imm, comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.bg2d] stream_left_site 0x{:08X} is not " @@ -944,7 +963,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) if (opcode == 0x08 || opcode == 0x09) { // addi / addiu uint32_t rs = get_rs(instr), rt = get_rt(instr); int16_t imm = get_imm16(instr); - return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_stream_right((int32_t){} + ({}));{}", + return fmt::format("{} = (uint32_t)psx_ws_bg2d_stream_right((int32_t){} + ({}));{}", reg_name(rt), reg_name(rs), imm, comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.bg2d] stream_right_site 0x{:08X} is not " @@ -2498,11 +2517,11 @@ std::string CodeGenerator::generate_file( ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n"; ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n"; ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n"; - ss << "extern int psx_ws_mmx6_bg_cols(int base); /* ws 2D bg tile-loop widen: col count (gpu.c) */\n"; - ss << "extern int psx_ws_mmx6_bg_startcol(int col); /* ws 2D bg tile-loop widen: start tile col (gpu.c) */\n"; - ss << "extern int psx_ws_mmx6_bg_startx(int x); /* ws 2D bg tile-loop widen: start screen-x (gpu.c) */\n"; - ss << "extern int psx_ws_mmx6_bg_stream_left(int x); /* ws 2D bg tile-ring streamer: left edge (gpu.c) */\n"; - ss << "extern int psx_ws_mmx6_bg_stream_right(int x); /* ws 2D bg tile-ring streamer: right edge (gpu.c) */\n"; + ss << "extern int psx_ws_bg2d_cols(int base); /* ws 2D bg tile-loop widen: col count (gpu.c) */\n"; + ss << "extern int psx_ws_bg2d_startcol(int col, unsigned mask); /* ws 2D bg tile-loop widen: start tile col (gpu.c) */\n"; + ss << "extern int psx_ws_bg2d_startx(int x); /* ws 2D bg tile-loop widen: start screen-x (gpu.c) */\n"; + ss << "extern int psx_ws_bg2d_stream_left(int x); /* ws 2D bg tile-ring streamer: left edge (gpu.c) */\n"; + ss << "extern int psx_ws_bg2d_stream_right(int x); /* ws 2D bg tile-ring streamer: right edge (gpu.c) */\n"; ss << "extern int psx_ws_mmx6_bg_bufbase(int addr); /* ws 2D bg packet-buffer relocation (gpu.c) */\n"; ss << "extern int psx_ws_mmx6_bg_undercap(int counter);/* ws 2D bg per-frame tile cap (gpu.c) */\n"; ss << "extern int psx_game_option_store(uint32_t addr, int val); /* persisted OPTION restore-at-init (game_options.c) */\n"; diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index a5eb6fd4..e69fcc1a 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -645,6 +645,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { // Optional [widescreen.cull] block — world-space draw-cull widening. std::vector ws_cull_bias_sites, ws_cull_range_sites, ws_cull_a1_sites; + std::vector ws_cull_screen_x_sites; bool ws_auto_screen_x_cull = false; bool ws_auto_backdrop_preload = false; if (cfg.contains("widescreen")) { @@ -659,6 +660,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { load_sites("bias_sites", ws_cull_bias_sites); load_sites("range_sites", ws_cull_range_sites); load_sites("a1_sites", ws_cull_a1_sites); + load_sites("screen_x_sites", ws_cull_screen_x_sites); if (cull.contains("auto_screen_x")) ws_auto_screen_x_cull = toml::find(cull, "auto_screen_x"); if (cull.contains("auto_backdrop")) @@ -670,6 +672,10 @@ GameConfig load_game_config(const fs::path& config_path_in) { uint32_t ws_bg2d_count_site = 0, ws_bg2d_startcol_site = 0, ws_bg2d_startx_site = 0; uint32_t ws_bg2d_stream_left_site = 0, ws_bg2d_stream_right_site = 0; uint32_t ws_bg2d_bufbase_site = 0, ws_bg2d_cap_site = 0; + uint32_t ws_bg2d_layer_base = 0x800971F8u, ws_bg2d_ring_base = 0x800A21B8u; + uint32_t ws_bg2d_map_size_addr = 0x800CD338u, ws_bg2d_layer_stride_addr = 0x8008EC10u; + uint32_t ws_bg2d_ring_cols = 64, ws_bg2d_layer_count = 3; + uint32_t ws_bg2d_layer_struct_stride = 0x54; if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("bg2d")) { @@ -687,6 +693,29 @@ GameConfig load_game_config(const fs::path& config_path_in) { ws_bg2d_stream_right_site = load1("stream_right_site"); ws_bg2d_bufbase_site = load1("bufbase_site"); ws_bg2d_cap_site = load1("cap_site"); + if (bg.contains("layer_base")) + ws_bg2d_layer_base = load1("layer_base"); + if (bg.contains("ring_base")) + ws_bg2d_ring_base = load1("ring_base"); + if (bg.contains("map_size_addr")) + ws_bg2d_map_size_addr = load1("map_size_addr"); + if (bg.contains("layer_stride_addr")) + ws_bg2d_layer_stride_addr = load1("layer_stride_addr"); + auto load_positive = [&](const char* key, uint32_t current) -> uint32_t { + if (!bg.contains(key)) return current; + const int64_t value = toml::find(bg, key); + if (value <= 0 || value > UINT32_MAX) + throw std::runtime_error(fmt::format( + "widescreen.bg2d.{} must be a positive 32-bit integer", key)); + return static_cast(value); + }; + ws_bg2d_ring_cols = load_positive("ring_cols", ws_bg2d_ring_cols); + ws_bg2d_layer_count = load_positive("layer_count", ws_bg2d_layer_count); + ws_bg2d_layer_struct_stride = load_positive( + "layer_struct_stride", ws_bg2d_layer_struct_stride); + if ((ws_bg2d_ring_cols & (ws_bg2d_ring_cols - 1u)) != 0u) + throw std::runtime_error( + "widescreen.bg2d.ring_cols must be a power of two"); } } @@ -735,6 +764,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_cull_bias_sites*/ ws_cull_bias_sites, /*ws_cull_range_sites*/ ws_cull_range_sites, /*ws_cull_a1_sites*/ ws_cull_a1_sites, + /*ws_cull_screen_x_sites*/ ws_cull_screen_x_sites, /*ws_backdrop_x_sites*/ ws_backdrop_x_sites, /*ws_backdrop_unsquash_funcs*/ ws_backdrop_unsquash_funcs, /*ws_auto_screen_x_cull*/ ws_auto_screen_x_cull, @@ -747,6 +777,13 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_bg2d_stream_right_site*/ ws_bg2d_stream_right_site, /*ws_bg2d_bufbase_site*/ ws_bg2d_bufbase_site, /*ws_bg2d_cap_site*/ ws_bg2d_cap_site, + /*ws_bg2d_layer_base*/ ws_bg2d_layer_base, + /*ws_bg2d_ring_base*/ ws_bg2d_ring_base, + /*ws_bg2d_map_size_addr*/ ws_bg2d_map_size_addr, + /*ws_bg2d_layer_stride_addr*/ ws_bg2d_layer_stride_addr, + /*ws_bg2d_ring_cols*/ ws_bg2d_ring_cols, + /*ws_bg2d_layer_count*/ ws_bg2d_layer_count, + /*ws_bg2d_layer_struct_stride*/ ws_bg2d_layer_struct_stride, }; } diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 8b566808..8cca8e68 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -392,6 +392,9 @@ struct GameConfig { std::vector ws_cull_bias_sites; std::vector ws_cull_range_sites; std::vector ws_cull_a1_sites; + // Explicit `sltiu rt,sx,W` render rejects for cases where codegen function + // splitting separates the paired vertical test from auto_screen_x. + std::vector ws_cull_screen_x_sites; // Backdrop screen-X squash ([widescreen.backdrop] x_sites). The parallax // 2D backdrop layer (ocean/cloud/mountain/grass — overlay actor handlers) @@ -451,8 +454,9 @@ struct GameConfig { // FUN_800270d0). Three instruction addresses in the per-layer BG renderer // whose column count and loop start are rewritten so the loop draws the // 16:9 reveal columns on both sides of the 320 view (see gpu.c - // psx_ws_mmx6_bg_* helpers — identity at 4:3 / 512 hi-res mode). Regen-class. - // count_site: the `li rt,21` column-count load (addiu/ori). + // psx_ws_bg2d_* helpers — identity at 4:3 / 512 hi-res mode). Regen-class. + // count_site: either the `li rt,21` column-count load (addiu/ori), or + // the loop-closing `sltiu rt,index,21` bound compare. // startcol_site: the `andi rt,rs,0x3f` start tile-column mask. // startx_site: the `sra rd,rt,sa` start screen-x. // 0 = unset (feature off). Verified by opcode at gen time. @@ -472,6 +476,17 @@ struct GameConfig { // raised to match the bigger buffer. Together they cure the dense-stage overflow. uint32_t ws_bg2d_bufbase_site = 0; uint32_t ws_bg2d_cap_site = 0; + + // Tile-ring layout used by the once-per-frame freshness refill. Defaults + // describe MMX6; sibling engines such as MMX5 override the shifted RAM + // addresses and smaller ring width in game.toml. + uint32_t ws_bg2d_layer_base = 0x800971F8u; + uint32_t ws_bg2d_ring_base = 0x800A21B8u; + uint32_t ws_bg2d_map_size_addr = 0x800CD338u; + uint32_t ws_bg2d_layer_stride_addr = 0x8008EC10u; + uint32_t ws_bg2d_ring_cols = 64; + uint32_t ws_bg2d_layer_count = 3; + uint32_t ws_bg2d_layer_struct_stride = 0x54; }; // UserSettings — the launcher-written, user-editable override layer. diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index 3b54a7f7..20ceee0a 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -120,6 +120,7 @@ int main(int argc, char** argv) { bool overlay_mode = false; 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_cull_screen_x; // [widescreen.cull] screen_x_sites std::set ws_backdrop_x; // [widescreen.backdrop] x_sites std::set ws_backdrop_unsquash; // [widescreen.backdrop] unsquash_funcs bool ws_auto_screen_x_cull = false; // [widescreen.cull] auto_screen_x @@ -144,6 +145,7 @@ int main(int argc, char** argv) { ws_cull_bias.insert(cfg.ws_cull_bias_sites.begin(), cfg.ws_cull_bias_sites.end()); 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_cull_screen_x.insert(cfg.ws_cull_screen_x_sites.begin(), cfg.ws_cull_screen_x_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()); ws_auto_screen_x_cull = ws_auto_screen_x_cull || cfg.ws_auto_screen_x_cull; @@ -218,6 +220,7 @@ int main(int argc, char** argv) { ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end()); 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_cull_screen_x.insert(wscfg.ws_cull_screen_x_sites.begin(), wscfg.ws_cull_screen_x_sites.end()); ws_backdrop_x.insert(wscfg.ws_backdrop_x_sites.begin(), wscfg.ws_backdrop_x_sites.end()); ws_backdrop_unsquash.insert(wscfg.ws_backdrop_unsquash_funcs.begin(), wscfg.ws_backdrop_unsquash_funcs.end()); ws_auto_screen_x_cull = ws_auto_screen_x_cull || wscfg.ws_auto_screen_x_cull; @@ -764,6 +767,7 @@ int main(int argc, char** argv) { codegen_config.ws_cull_bias_sites = ws_cull_bias; codegen_config.ws_cull_range_sites = ws_cull_range; codegen_config.ws_cull_a1_sites = ws_cull_a1; + codegen_config.ws_cull_screen_x_sites = ws_cull_screen_x; codegen_config.ws_backdrop_x_sites = ws_backdrop_x; codegen_config.ws_backdrop_unsquash_funcs = ws_backdrop_unsquash; codegen_config.ws_auto_screen_x_cull = ws_auto_screen_x_cull; diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index fa017e92..a7d9a2ab 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -98,9 +98,19 @@ void gpu_ws_configure(int aspect_num, int aspect_den, /* [widescreen] full_2d: opt a pure-2D sprite game into the widescreen present * path (treat every in-game frame as gameplay, since it never tags 3D prims). */ void gpu_ws_set_full_2d(int on); -/* [widescreen.bg2d] MMX6 background tile-loop widen — hooked at the renderer's +/* [widescreen.bg2d] Capcom 2D background tile-loop widen — hooked at the renderer's * column-count / start-tile-col / start-screen-x instructions. Identity at 4:3 * and in the engine's 512 hi-res mode. */ +void gpu_ws_bg2d_configure(uint32_t layer_base, uint32_t ring_base, + uint32_t map_size_addr, uint32_t layer_stride_addr, + uint32_t ring_cols, uint32_t layer_count, + uint32_t layer_struct_stride); +int psx_ws_bg2d_cols(int base); +int psx_ws_bg2d_startcol(int col, unsigned mask); +int psx_ws_bg2d_startx(int x); +int psx_ws_bg2d_stream_left(int x); +int psx_ws_bg2d_stream_right(int x); +/* Compatibility entry points for already-generated MMX6 sources. */ int psx_ws_mmx6_bg_cols(int base); int psx_ws_mmx6_bg_startcol(int col); int psx_ws_mmx6_bg_startx(int x); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index b307a1f3..e3a244be 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -201,8 +201,8 @@ int psx_ws_x_margin(void) { return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum; } -/* ---- MMX6 2D background tile-loop widen ([widescreen.bg2d]) --------------- - * Mega Man X6 is a pure-2D sprite engine that renders only a 4:3 (320px) field +/* ---- Capcom 2D background tile-loop widen ([widescreen.bg2d]) -------------- + * Mega Man X5/X6 use a pure-2D sprite engine that renders only a 4:3 (320px) field * of view — there is no overscan to "reveal." Its per-layer background renderer * (FUN_800270d0) draws `count` 16px tile columns × 16 rows from a start tile * column / start screen-x derived from the camera scroll. To produce a TRUE @@ -220,7 +220,29 @@ int psx_ws_x_margin(void) { * are prepended left and appended right. The 64-column tile ring (mask 0x3f) is * far wider than the visible window, so the prepended columns fetch the correct * already-streamed adjacent tiles. */ -static int ws_mmx6_left_cols(void) { +static uint32_t g_bg2d_layer_base = 0x800971F8u; +static uint32_t g_bg2d_ring_base = 0x800A21B8u; +static uint32_t g_bg2d_map_size_addr = 0x800CD338u; +static uint32_t g_bg2d_layer_stride_addr = 0x8008EC10u; +static uint32_t g_bg2d_ring_cols = 64; +static uint32_t g_bg2d_layer_count = 3; +static uint32_t g_bg2d_layer_struct_stride = 0x54; +static int g_bg2d_native_cols = 21; + +void gpu_ws_bg2d_configure(uint32_t layer_base, uint32_t ring_base, + uint32_t map_size_addr, uint32_t layer_stride_addr, + uint32_t ring_cols, uint32_t layer_count, + uint32_t layer_struct_stride) { + g_bg2d_layer_base = layer_base; + g_bg2d_ring_base = ring_base; + g_bg2d_map_size_addr = map_size_addr; + g_bg2d_layer_stride_addr = layer_stride_addr; + g_bg2d_ring_cols = ring_cols; + g_bg2d_layer_count = layer_count; + g_bg2d_layer_struct_stride = layer_struct_stride; +} + +static int ws_bg2d_left_cols(void) { if (!ws_native_wide_active()) return 0; /* Only the ~320 gameplay mode; the engine's 512 hi-res mode (title) draws * its own 33 columns and centres itself — never double-shift it. */ @@ -245,16 +267,22 @@ static int mmx6_hostside_active(void) { return g_mmx6_hostside && ws_native_wide_active() && ws_disp_w() <= 384 && ws_nw_offset() > 0; } -/* Column count: native (21) under host-side mode; else base + both-side reveal. - * Also drives the once-per-frame ring-freshness refill (mmx6_bg_refill_tick): this - * hook fires at the start of each layer's render, so the first call of a new frame - * re-streams the widened window for all 3 layers right before they are drawn. */ +/* Column count: native (21) under host-side mode; else base + both-side reveal. */ static void mmx6_bg_refill_tick(void); /* defined below (ring-freshness fix) */ -int psx_ws_mmx6_bg_cols(int base) { mmx6_bg_refill_tick(); return mmx6_hostside_active() ? base : base + 2 * ws_mmx6_left_cols(); } -/* Start tile column: native under host-side mode; else LEFT earlier (64-col ring). */ -int psx_ws_mmx6_bg_startcol(int col) { return mmx6_hostside_active() ? col : ((col - ws_mmx6_left_cols()) & 0x3f); } +int psx_ws_bg2d_cols(int base) { + g_bg2d_native_cols = base; + mmx6_bg_refill_tick(); + return mmx6_hostside_active() ? base : base + 2 * ws_bg2d_left_cols(); +} +/* Start tile column: refill before the first column is consumed, then begin LEFT + * earlier in the ring. The count hook retains the same tick for layouts that load + * their loop bound before calculating the starting column. */ +int psx_ws_bg2d_startcol(int col, unsigned mask) { + mmx6_bg_refill_tick(); + return mmx6_hostside_active() ? col : ((col - ws_bg2d_left_cols()) & (int)mask); +} /* Start screen-x: native under host-side mode; else LEFT*16 further left. */ -int psx_ws_mmx6_bg_startx(int x) { return mmx6_hostside_active() ? x : (x - ws_mmx6_left_cols() * 16); } +int psx_ws_bg2d_startx(int x) { return mmx6_hostside_active() ? x : (x - ws_bg2d_left_cols() * 16); } /* Tile-RING STREAMER widen (same [widescreen.bg2d], FUN_800273e4). The renderer * above now draws LEFT extra columns each side, but the engine's 64-column tile @@ -266,8 +294,15 @@ int psx_ws_mmx6_bg_startx(int x) { return mmx6_hostside_active() ? x : (x - * the camera scrolls (the incremental streamer fills each newly-leading column). * Identity (no extra streaming) at 4:3 / 512 hi-res, so the ring is byte-identical * there. The 64-col ring has ample slack (visible ~21 cols) for ±LEFT more. */ -int psx_ws_mmx6_bg_stream_left(int x) { return x - ws_mmx6_left_cols() * 16; } -int psx_ws_mmx6_bg_stream_right(int x) { return x + ws_mmx6_left_cols() * 16; } +int psx_ws_bg2d_stream_left(int x) { return x - ws_bg2d_left_cols() * 16; } +int psx_ws_bg2d_stream_right(int x) { return x + ws_bg2d_left_cols() * 16; } + +/* Compatibility for generated MMX6 sources predating the generic names. */ +int psx_ws_mmx6_bg_cols(int base) { return psx_ws_bg2d_cols(base); } +int psx_ws_mmx6_bg_startcol(int col) { return psx_ws_bg2d_startcol(col, 0x3fu); } +int psx_ws_mmx6_bg_startx(int x) { return psx_ws_bg2d_startx(x); } +int psx_ws_mmx6_bg_stream_left(int x) { return psx_ws_bg2d_stream_left(x); } +int psx_ws_mmx6_bg_stream_right(int x) { return psx_ws_bg2d_stream_right(x); } /* ===== MMX6 BG packet-buffer RELOCATION ([widescreen.bg2d] bufbase_site/cap_site) === * The widened BG render (29 cols) overruns the engine's BG packet double-buffer (driver @@ -295,7 +330,7 @@ static int g_mmx6_reloc = 0; void gpu_ws_mmx6_set_reloc(int on) { g_mmx6_reloc = on ? 1 : 0; } int gpu_ws_mmx6_reloc_get(void) { return g_mmx6_reloc; } static int mmx6_bg_reloc_active(void) { - return g_mmx6_reloc && !g_mmx6_hostside && ws_mmx6_left_cols() > 0; + return g_mmx6_reloc && !g_mmx6_hostside && ws_bg2d_left_cols() > 0; } /* Hook at the driver's buffer-address addu (0x80026DC4): remap base 0x800B91C0 + bufidx*0x4000 @@ -392,20 +427,21 @@ int gpu_ws_mmx6_clut_count(void) { * vertical band being cleared (band_y / base_x from the GP0 fill). Pure guest reads. */ void gpu_ws_mmx6_emit_reveal(int base_x, int band_y) { if (!mmx6_hostside_active() || !g_mmx6_clut) return; - int left = ws_mmx6_left_cols(); + int left = ws_bg2d_left_cols(); if (left <= 0) return; uint32_t attr_base = psx_read_word(0x1F80000Cu); if (attr_base == 0) return; g_mmx6_emit_calls++; g_mmx6_emit_tiles = 0; sw_wide_set_target(base_x); /* g_wide_cur = this band's surface */ - for (int layer = 0; layer < 3; layer++) { - uint32_t ls = 0x800971F8u + (uint32_t)layer * 0x54u; + for (uint32_t layer = 0; layer < g_bg2d_layer_count; layer++) { + uint32_t ls = g_bg2d_layer_base + layer * g_bg2d_layer_struct_stride; int sx = (int16_t)psx_read_half(ls + 0xa); int sy = (int16_t)psx_read_half(ls + 0xe); int8_t parent = (int8_t)psx_read_byte(ls + 0x52); if (parent >= 0) { /* linked layer: add parent's scroll */ - uint32_t lp = 0x800971F8u + (uint32_t)(uint8_t)parent * 0x54u; + uint32_t lp = g_bg2d_layer_base + + (uint32_t)(uint8_t)parent * g_bg2d_layer_struct_stride; sx += (int16_t)psx_read_half(lp + 0xa); sy += (int16_t)psx_read_half(lp + 0xe); } @@ -413,9 +449,9 @@ void gpu_ws_mmx6_emit_reveal(int base_x, int band_y) { int sub_x = -(sx & 0xf); int syr = sy; if (syr < 0) syr += 0xf; int start_row = syr >> 4; int sub_y = -(sy & 0xf); - uint32_t ring = 0x800A21B8u + (uint32_t)layer * 0x1000u; + uint32_t ring = g_bg2d_ring_base + layer * (g_bg2d_ring_cols * 64u); for (int side = 0; side < 2; side++) { - int ci0 = (side == 0) ? -left : 21; /* native draws cols 0..20 */ + int ci0 = (side == 0) ? -left : g_bg2d_native_cols; for (int k = 0; k < left; k++) { int ci = ci0 + k; /* column index relative to native start */ int col = start_col + ci; @@ -423,8 +459,8 @@ void gpu_ws_mmx6_emit_reveal(int base_x, int band_y) { for (int row = 0; row < 16; row++) { int trow = start_row + row; uint16_t tile = psx_read_half(ring - + (uint32_t)((col & 0x3f) * 2) - + (uint32_t)((trow & 0x1f) * 0x80)); + + (uint32_t)((col & (int)(g_bg2d_ring_cols - 1u)) * 2) + + (uint32_t)((trow & 0x1f) * (int)(g_bg2d_ring_cols * 2u))); if (tile == 0) continue; uint32_t attr = psx_read_word(attr_base + (uint32_t)((tile & 0x3fff) * 4)); if ((attr >> 0x18) == 0xff) continue; @@ -470,16 +506,25 @@ extern void psx_write_half(uint32_t addr, uint16_t val); * (UNSIGNED). When write!=0 the tiles are written to the ring; when write==0 they are * compared against the live ring (validation), counting *cmp_total / *cmp_bad. Returns * 1 if the column was streamed, 0 if skipped (map-loop seam early-return). */ -static int mmx6_fill_column(int layer, int worldX, int worldY, int write, - int *cmp_total, int *cmp_bad) { - uint32_t lbase = 0x800971F8u + (uint32_t)layer * 0x54u; +static int bg2d_fill_column(int layer, int worldX, int worldY, int write, + int *cmp_total, int *cmp_bad) { + /* The guest column streamer returns immediately for negative world X. Do + * not wrap it into the far edge of the map, which produces repeated blocks + * in the left reveal near a level boundary. */ + if (worldX < 0) return 0; + uint32_t lbase = g_bg2d_layer_base + (uint32_t)layer * g_bg2d_layer_struct_stride; int rows = 0x12; if (worldY < 0) { worldY = 0; rows = 0x11; } int32_t metaCol = ((worldX < 0) ? worldX + 0xff : worldX) >> 8; /* t1 */ int32_t metaRow = ((worldY < 0) ? worldY + 0xff : worldY) >> 8; /* t4 */ - int wrapX = worldX - (((worldX < 0) ? worldX + 0x3ff : worldX) >> 10) * 0x400; + int worldPeriod = (int)g_bg2d_ring_cols * 16; + int worldShift = 0; + for (int n = worldPeriod; n > 1; n >>= 1) worldShift++; + int wrapX = worldX + - (((worldX < 0) ? worldX + worldPeriod - 1 : worldX) >> worldShift) + * worldPeriod; int colsh = ((wrapX < 0) ? wrapX + 0xf : wrapX) >> 4; /* t7 = wrapX>>4 */ - int ringcol = colsh & 0x3f; /* masked (renderer reads &0x3f) */ + int ringcol = colsh & (int)(g_bg2d_ring_cols - 1u); int tcolInMeta = colsh & 0xf; /* s0 = t7 & 0xf */ int wrapY = worldY - (((worldY < 0) ? worldY + 0x1ff : worldY) >> 9) * 0x200; int ringrow = ((wrapY < 0) ? wrapY + 0xf : wrapY) >> 4; /* a2 */ @@ -496,19 +541,22 @@ static int mmx6_fill_column(int layer, int worldX, int worldY, int write, metaCol = (metaCol - 1 - mapRight) + mapLeft; if ((mapRight + 1) * 0x100 <= scrollX) return 0; } - int mapW = psx_read_byte(0x800CD338u); - int mapH = psx_read_byte(0x800CD339u); + int mapW = psx_read_byte(g_bg2d_map_size_addr); + int mapH = psx_read_byte(g_bg2d_map_size_addr + 1u); uint32_t mapBase = psx_read_word(0x1F800004u); uint32_t metaBase = psx_read_word(0x1F800008u); - int layerStride = (uint16_t)psx_read_half(0x8008EC10u); + if (mapBase == 0 || metaBase == 0 || mapW <= 0 || mapH <= 0) return 0; + int layerStride = (uint16_t)psx_read_half(g_bg2d_layer_stride_addr); int metaIdx = 0; if ((uint32_t)metaCol < (uint32_t)mapW && (uint32_t)metaRow < (uint32_t)mapH) metaIdx = psx_read_byte(mapBase + (uint32_t)(layer * layerStride + mapW * metaRow + metaCol)); - uint32_t ringbase = 0x800A21B8u + (uint32_t)layer * 0x1000u; + uint32_t ringbase = g_bg2d_ring_base + + (uint32_t)layer * (g_bg2d_ring_cols * 64u); for (int i = 0; i < rows; i++) { - uint32_t cell = ringbase + (uint32_t)(ringcol * 2) + (uint32_t)((ringrow & 0x1f) * 0x80); + uint32_t cell = ringbase + (uint32_t)(ringcol * 2) + + (uint32_t)((ringrow & 0x1f) * (int)(g_bg2d_ring_cols * 2u)); uint16_t tile = psx_read_half(metaBase + (uint32_t)metaIdx * 0x200u + (uint32_t)(trowInMeta * 0x20) + (uint32_t)(tcolInMeta * 2)); if (write) { @@ -536,32 +584,35 @@ void gpu_ws_mmx6_set_freshfix(int on) { g_mmx6_freshfix = on ? 1 : 0; } int gpu_ws_mmx6_freshfix_get(void) { return g_mmx6_freshfix; } long gpu_ws_mmx6_refill_cols(void) { return g_mmx6_refill_cols; } -/* Re-stream every tile column of the widened window for all 3 BG layers, using each - * layer's OWN scroll (the streamer driver keys the ring on the layer's own scrollX/ - * scrollY, not the parent-coupled value the renderer reads with) and worldY = scrollY - * - 0x10 (mirrors the driver). ci spans the renderer's widened span [-LEFT, 20+LEFT]. +/* Re-stream every tile column of the widened window for each independently streamed + * BG layer, using its OWN scroll and worldY = scrollY - 0x10 (mirrors the driver). + * Parent-linked layers are skipped exactly as they are by the guest streamer. ci + * spans the renderer's widened span [-LEFT, 20+LEFT]. * No-op at 4:3 / 512 hi-res (left==0). Idempotent over the native columns the engine * already streamed correctly, so re-streaming them is harmless. */ void psx_ws_mmx6_bg_refill_all(void) { if (!g_mmx6_freshfix) return; if (!ws_native_wide_active() || ws_disp_w() > 384) return; - int left = ws_mmx6_left_cols(); + int left = ws_bg2d_left_cols(); if (left <= 0) return; g_mmx6_refill_cols = 0; - for (int layer = 0; layer < 3; layer++) { - uint32_t lbase = 0x800971F8u + (uint32_t)layer * 0x54u; + for (uint32_t layer = 0; layer < g_bg2d_layer_count; layer++) { + uint32_t lbase = g_bg2d_layer_base + layer * g_bg2d_layer_struct_stride; + /* Match the guest driver: parent-linked layers are not incrementally + * streamed and must retain their prefilled ring contents. */ + if ((int8_t)psx_read_byte(lbase + 0x52) >= 0) continue; int sx = (int16_t)psx_read_half(lbase + 0xa); int sy = (int16_t)psx_read_half(lbase + 0xe); - for (int ci = -left; ci <= 20 + left; ci++) { - if (mmx6_fill_column(layer, sx + ci * 16, sy - 0x10, 1, NULL, NULL)) + for (int ci = -left; ci < g_bg2d_native_cols + left; ci++) { + if (bg2d_fill_column((int)layer, sx + ci * 16, sy - 0x10, 1, NULL, NULL)) g_mmx6_refill_cols++; } } } -/* Trigger the once-per-frame widened-window refill. Called from the count_site hook - * (psx_ws_mmx6_bg_cols), which fires at the start of each layer's render — the first - * call of a new frame refills all 3 layers right before they are drawn. */ +/* Trigger the once-per-frame widened-window refill. Both renderer setup hooks call + * this so differing instruction order between games still refreshes the ring before + * the first widened column is consumed. */ static void mmx6_bg_refill_tick(void) { uint32_t f = (uint32_t)s_frame_count; if (f == g_mmx6_refill_frame) return; @@ -575,12 +626,13 @@ static void mmx6_bg_refill_tick(void) { * ring) is trusted. Returns total cells compared; *bad = mismatches. */ int gpu_ws_mmx6_validate(int *bad_out) { int total = 0, bad = 0; - for (int layer = 0; layer < 3; layer++) { - uint32_t lbase = 0x800971F8u + (uint32_t)layer * 0x54u; + for (uint32_t layer = 0; layer < g_bg2d_layer_count; layer++) { + uint32_t lbase = g_bg2d_layer_base + layer * g_bg2d_layer_struct_stride; + if ((int8_t)psx_read_byte(lbase + 0x52) >= 0) continue; int sx = (int16_t)psx_read_half(lbase + 0xa); int sy = (int16_t)psx_read_half(lbase + 0xe); - for (int ci = 0; ci <= 20; ci++) - mmx6_fill_column(layer, sx + ci * 16, sy - 0x10, 0, &total, &bad); + for (int ci = 0; ci < g_bg2d_native_cols; ci++) + bg2d_fill_column((int)layer, sx + ci * 16, sy - 0x10, 0, &total, &bad); } if (bad_out) *bad_out = bad; return total; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index de56c9c1..56f3cb70 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2062,6 +2062,15 @@ int main(int argc, char** argv) { * widescreen present path. Applied to the GPU layer up front so the * ws engage at game entry classifies every frame as gameplay. */ gpu_ws_set_full_2d(gc.ws_full_2d ? 1 : 0); + /* [widescreen.bg2d] engine tile-ring layout for the freshness + * refill shared by the MMX5/MMX6 2D background hook. */ + gpu_ws_bg2d_configure(gc.ws_bg2d_layer_base, + gc.ws_bg2d_ring_base, + gc.ws_bg2d_map_size_addr, + gc.ws_bg2d_layer_stride_addr, + gc.ws_bg2d_ring_cols, + gc.ws_bg2d_layer_count, + gc.ws_bg2d_layer_struct_stride); /* Register the [widescreen.backdrop] store PCs so the dirty-RAM * interpreter applies the backdrop screenX squash on the interp * path (overlay backdrop handlers run interpreted when no cache diff --git a/tools/regen_bios.sh b/tools/regen_bios.sh index 8437f699..48229c78 100644 --- a/tools/regen_bios.sh +++ b/tools/regen_bios.sh @@ -42,6 +42,7 @@ EXE="$BUILD/psxrecomp-bios.exe"; [ -f "$EXE" ] || EXE="$BUILD/psxrecomp-bios" # 2. Regenerate the BIOS C. echo "regen_bios: emit-full $BIOS -> $OUT (seeds: $SEEDS)" +mkdir -p "$OUT" "$EXE" "$BIOS" "$OUT" --emit-full "$SEEDS" # 3. Record the emitter fingerprint so the build can detect future drift. MUST From 40b476c4e4d2d7052d94dbdf408f9c612aa17d8c Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 10 Jul 2026 10:07:40 -0700 Subject: [PATCH 2/2] widescreen: finish MMX5 edge handling --- recompiler/include/code_generator.h | 3 +- recompiler/src/code_generator.cpp | 14 +++- recompiler/src/config_loader.cpp | 3 + recompiler/src/config_loader.h | 5 +- runtime/include/gpu.h | 3 +- runtime/src/gpu.c | 117 ++++++++++++++++++++++++---- runtime/src/main.cpp | 3 +- 7 files changed, 123 insertions(+), 25 deletions(-) diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index 4c2795fa..29aeec4d 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -65,7 +65,8 @@ struct CodeGenConfig { // addresses the immediate is emitted with a runtime margin term // psx_ws_x_margin() so the world-space draw cull widens with the aspect // (0 at 4:3). bias = addiu (+margin); range = sltiu (+2*margin); a1 = a - // nop replaced with `a1 += margin`. See config_loader.h. Empty = default. + // nop replaced with `a1 += margin`, or move rD,a1 replaced with + // `rD = a1 + margin`. See config_loader.h. Empty = default. std::set ws_cull_bias_sites; std::set ws_cull_range_sites; std::set ws_cull_a1_sites; diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 1c3b3443..4351840c 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -837,9 +837,16 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) // a1 = $5: widen the caller-supplied margin (param-margin classifiers). return fmt::format("cpu->gpr[5] = cpu->gpr[5] + psx_ws_x_margin();" " /* ws cull a1 bias */{}", comment); + } else if (opcode == 0x00 && get_funct(instr) == 0x21 && + get_rs(instr) == 5 && get_rt(instr) == 0) { + // move rD,a1 (addu rD,a1,zero): widen the copied classifier margin. + uint32_t rd = get_rd(instr); + return fmt::format("{} = cpu->gpr[5] + psx_ws_x_margin();" + " /* ws cull copied a1 bias */{}", + reg_name(rd), comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.cull] a1 site 0x{:08X} is not a " - "nop (0x{:08X})\n", addr, instr); + "nop or move rD,a1 (0x{:08X})\n", addr, instr); std::exit(1); } // overlay variant: addr is different code here — fall through to vanilla. @@ -1021,8 +1028,8 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) if (config_.ws_bg2d_cap_site && addr == config_.ws_bg2d_cap_site) { if (opcode == 0x0A) { // slti rt,rs,imm (BG per-frame tile cap compare) uint32_t rs = get_rs(instr), rt = get_rt(instr); - return fmt::format("{} = (uint32_t)psx_ws_mmx6_bg_undercap((int32_t){});{}", - reg_name(rt), reg_name(rs), comment); + return fmt::format("{} = (uint32_t)psx_ws_bg2d_undercap((int32_t){}, {});{}", + reg_name(rt), reg_name(rs), (int16_t)(instr & 0xFFFFu), comment); } else if (!config_.overlay_mode) { fmt::print(stderr, "ERROR: [widescreen.bg2d] cap_site 0x{:08X} is not " "slti (opcode 0x{:02X})\n", addr, opcode); @@ -2631,6 +2638,7 @@ std::string CodeGenerator::generate_file( ss << "extern int psx_ws_bg2d_startx(int x); /* ws 2D bg tile-loop widen: start screen-x (gpu.c) */\n"; ss << "extern int psx_ws_bg2d_stream_left(int x); /* ws 2D bg tile-ring streamer: left edge (gpu.c) */\n"; ss << "extern int psx_ws_bg2d_stream_right(int x); /* ws 2D bg tile-ring streamer: right edge (gpu.c) */\n"; + ss << "extern int psx_ws_bg2d_undercap(int counter, int native_cap); /* ws 2D BG packet cap (gpu.c) */\n"; ss << "extern int psx_ws_mmx6_bg_bufbase(int addr); /* ws 2D bg packet-buffer relocation (gpu.c) */\n"; ss << "extern int psx_ws_mmx6_bg_undercap(int counter);/* ws 2D bg per-frame tile cap (gpu.c) */\n"; ss << "extern int psx_game_option_store(uint32_t addr, int val); /* persisted OPTION restore-at-init (game_options.c) */\n"; diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 67f37c74..b22021e4 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -814,6 +814,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { uint32_t ws_bg2d_ring_cols = 64, ws_bg2d_layer_count = 3; uint32_t ws_bg2d_layer_struct_stride = 0x54; uint32_t ws_bg2d_init_func = 0; + uint32_t ws_bg2d_packet_cap = 1000; if (cfg.contains("widescreen")) { const toml::value& ws = toml::find(cfg, "widescreen"); if (ws.contains("bg2d")) { @@ -851,6 +852,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { ws_bg2d_layer_count = load_positive("layer_count", ws_bg2d_layer_count); ws_bg2d_layer_struct_stride = load_positive( "layer_struct_stride", ws_bg2d_layer_struct_stride); + ws_bg2d_packet_cap = load_positive("packet_cap", ws_bg2d_packet_cap); if ((ws_bg2d_ring_cols & (ws_bg2d_ring_cols - 1u)) != 0u) throw std::runtime_error( "widescreen.bg2d.ring_cols must be a power of two"); @@ -936,6 +938,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { /*ws_bg2d_layer_count*/ ws_bg2d_layer_count, /*ws_bg2d_layer_struct_stride*/ ws_bg2d_layer_struct_stride, /*ws_bg2d_init_func*/ ws_bg2d_init_func, + /*ws_bg2d_packet_cap*/ ws_bg2d_packet_cap, }; } diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index cb5225c0..d94c879b 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -410,8 +410,8 @@ struct GameConfig { // ~the half-extra-width when stretching) into the relevant immediates: // cull_bias_sites: an addiu rT,rS,imm → rT = rS + (imm + margin) // cull_range_sites: an sltiu rT,rS,imm → rT = rS ws_cull_bias_sites; std::vector ws_cull_range_sites; @@ -577,6 +577,7 @@ struct GameConfig { // layer's stage data is dirty. A callback at entry invalidates stale host // reveal pixels once before the new stage background is submitted. uint32_t ws_bg2d_init_func = 0; + uint32_t ws_bg2d_packet_cap = 1000; }; // UserSettings — the launcher-written, user-editable override layer. diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 709f373d..cdd2d457 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -112,12 +112,13 @@ void gpu_ws_set_full_2d(int on); void gpu_ws_bg2d_configure(uint32_t layer_base, uint32_t ring_base, uint32_t map_size_addr, uint32_t layer_stride_addr, uint32_t ring_cols, uint32_t layer_count, - uint32_t layer_struct_stride); + uint32_t layer_struct_stride, uint32_t packet_cap); int psx_ws_bg2d_cols(int base); int psx_ws_bg2d_startcol(int col, unsigned mask); int psx_ws_bg2d_startx(int x); int psx_ws_bg2d_stream_left(int x); int psx_ws_bg2d_stream_right(int x); +int psx_ws_bg2d_undercap(int counter, int native_cap); /* Compatibility entry points for already-generated MMX6 sources. */ int psx_ws_mmx6_bg_cols(int base); int psx_ws_mmx6_bg_startcol(int col); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 2ecdbfd4..c36b3382 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -300,12 +300,13 @@ static uint32_t g_bg2d_layer_stride_addr = 0x8008EC10u; static uint32_t g_bg2d_ring_cols = 64; static uint32_t g_bg2d_layer_count = 3; static uint32_t g_bg2d_layer_struct_stride = 0x54; +static uint32_t g_bg2d_packet_cap = 1000; static int g_bg2d_native_cols = 21; void gpu_ws_bg2d_configure(uint32_t layer_base, uint32_t ring_base, uint32_t map_size_addr, uint32_t layer_stride_addr, uint32_t ring_cols, uint32_t layer_count, - uint32_t layer_struct_stride) { + uint32_t layer_struct_stride, uint32_t packet_cap) { g_bg2d_layer_base = layer_base; g_bg2d_ring_base = ring_base; g_bg2d_map_size_addr = map_size_addr; @@ -313,6 +314,7 @@ void gpu_ws_bg2d_configure(uint32_t layer_base, uint32_t ring_base, g_bg2d_ring_cols = ring_cols; g_bg2d_layer_count = layer_count; g_bg2d_layer_struct_stride = layer_struct_stride; + g_bg2d_packet_cap = packet_cap; } static int ws_bg2d_left_cols(void) { @@ -369,6 +371,10 @@ int psx_ws_bg2d_startx(int x) { return mmx6_hostside_active() ? x : (x - * there. The 64-col ring has ample slack (visible ~21 cols) for ±LEFT more. */ int psx_ws_bg2d_stream_left(int x) { return x - ws_bg2d_left_cols() * 16; } int psx_ws_bg2d_stream_right(int x) { return x + ws_bg2d_left_cols() * 16; } +int psx_ws_bg2d_undercap(int counter, int native_cap) { + int cap = ws_bg2d_left_cols() > 0 ? (int)g_bg2d_packet_cap : native_cap; + return counter < cap; +} /* Compatibility for generated MMX6 sources predating the generic names. */ int psx_ws_mmx6_bg_cols(int base) { return psx_ws_bg2d_cols(base); } @@ -452,8 +458,8 @@ int psx_ws_mmx6_bg_bufbase(int addr) { /* Hook at the BG cap slti (0x80027278): counter < cap. Raises the cap to MMX6_RELOC_CAP when * relocated (the bigger buffer can hold them); plain 1000 otherwise (byte-identical). */ int psx_ws_mmx6_bg_undercap(int counter) { - int cap = mmx6_bg_reloc_active() ? MMX6_RELOC_CAP : 1000; - return (counter < cap) ? 1 : 0; + if (mmx6_bg_reloc_active()) return counter < MMX6_RELOC_CAP; + return psx_ws_bg2d_undercap(counter, 1000); } /* ===== MMX6 host-side reveal columns ====================================== * @@ -587,12 +593,28 @@ void gpu_ws_mmx6_emit_reveal(int base_x, int band_y) { * native-wide + ~320 mode, so 4:3 and the 512 hi-res title stay byte-identical. */ extern void psx_write_half(uint32_t addr, uint16_t val); +static void bg2d_clear_column(int layer, int ringcol, int worldY) { + int rows = 0x12; + if (worldY < 0) { worldY = 0; rows = 0x11; } + int wrapY = worldY - (((worldY < 0) ? worldY + 0x1ff : worldY) >> 9) * 0x200; + int ringrow = ((wrapY < 0) ? wrapY + 0xf : wrapY) >> 4; + uint32_t ringbase = g_bg2d_ring_base + + (uint32_t)layer * (g_bg2d_ring_cols * 64u); + for (int i = 0; i < rows; i++) { + uint32_t cell = ringbase + + (uint32_t)((ringcol & (int)(g_bg2d_ring_cols - 1u)) * 2) + + (uint32_t)((ringrow & 0x1f) * (int)(g_bg2d_ring_cols * 2u)); + psx_write_half(cell, 0); + ringrow = (ringrow + 1) & 0x1f; + } +} + /* Faithful clone of FUN_800274a0's inner fill: stream one tile column at guest pixel * (worldX, worldY) of `layer` into the 64x32 ring. Compares mirror the engine's sltu * (UNSIGNED). When write!=0 the tiles are written to the ring; when write==0 they are * compared against the live ring (validation), counting *cmp_total / *cmp_bad. Returns * 1 if the column was streamed, 0 if skipped (map-loop seam early-return). */ -static int bg2d_fill_column(int layer, int worldX, int worldY, int write, +static int bg2d_fill_column(int layer, int worldX, int worldY, int scrollX, int write, int *cmp_total, int *cmp_bad) { /* The guest column streamer returns immediately for negative world X. Do * not wrap it into the far edge of the map, which produces repeated blocks @@ -618,7 +640,6 @@ static int bg2d_fill_column(int layer, int worldX, int worldY, int write, int mapLeft = psx_read_byte(lbase + 0x4d); int mapRight = psx_read_byte(lbase + 0x4e); - int scrollX = (int16_t)psx_read_half(lbase + 0xa); /* C integer division toward zero makes worldX -255..-1 resolve to metatile * column 0 below. The native 4:3 streamer never requests those coordinates, * but the widened left window does at a stage starting on mapLeft. Treat @@ -683,10 +704,12 @@ void gpu_ws_mmx6_set_freshfix(int on) { g_mmx6_freshfix = on ? 1 : 0; } int gpu_ws_mmx6_freshfix_get(void) { return g_mmx6_freshfix; } long gpu_ws_mmx6_refill_cols(void) { return g_mmx6_refill_cols; } -/* Re-stream every tile column of the widened window for each independently streamed - * BG layer, using its OWN scroll and worldY = scrollY - 0x10 (mirrors the driver). - * Parent-linked layers are skipped exactly as they are by the guest streamer. ci - * spans the renderer's widened span [-LEFT, 20+LEFT]. +/* Re-stream every tile column of the widened window for every BG layer. Independent + * layers use their own scroll; parent-linked layers use the same combined scroll + * that the renderer uses to index their ring. The guest's incremental streamer + * skips linked layers because their native 4:3 window is prefilled, but the newly + * revealed columns still need to be populated here. worldY = scrollY - 0x10 mirrors + * the driver. ci spans the renderer's widened span [-LEFT, 20+LEFT]. * No-op at 4:3 / 512 hi-res (left==0). Idempotent over the native columns the engine * already streamed correctly, so re-streaming them is harmless. */ void psx_ws_mmx6_bg_refill_all(void) { @@ -697,15 +720,26 @@ void psx_ws_mmx6_bg_refill_all(void) { g_mmx6_refill_cols = 0; for (uint32_t layer = 0; layer < g_bg2d_layer_count; layer++) { uint32_t lbase = g_bg2d_layer_base + layer * g_bg2d_layer_struct_stride; - /* Match the guest driver: parent-linked layers are not incrementally - * streamed and must retain their prefilled ring contents. */ - if ((int8_t)psx_read_byte(lbase + 0x52) >= 0) continue; int sx = (int16_t)psx_read_half(lbase + 0xa); int sy = (int16_t)psx_read_half(lbase + 0xe); + int8_t parent = (int8_t)psx_read_byte(lbase + 0x52); + if (parent >= 0 && (uint32_t)(uint8_t)parent < g_bg2d_layer_count) { + uint32_t pbase = g_bg2d_layer_base + + (uint32_t)(uint8_t)parent * g_bg2d_layer_struct_stride; + sx += (int16_t)psx_read_half(pbase + 0xa); + sy += (int16_t)psx_read_half(pbase + 0xe); + } + int sxr = sx; + if (sxr < 0) sxr += 0xf; + int start_col = sxr >> 4; for (int ci = -left; ci < g_bg2d_native_cols + left; ci++) { - int filled = bg2d_fill_column((int)layer, sx + ci * 16, - sy - 0x10, 1, NULL, NULL); - if (filled) + int world_x = sx + ci * 16; + if (world_x < 0) { + bg2d_clear_column((int)layer, start_col + ci, sy - 0x10); + continue; + } + if (bg2d_fill_column((int)layer, world_x, sy - 0x10, + sx, 1, NULL, NULL)) g_mmx6_refill_cols++; } } @@ -729,11 +763,18 @@ int gpu_ws_mmx6_validate(int *bad_out) { int total = 0, bad = 0; for (uint32_t layer = 0; layer < g_bg2d_layer_count; layer++) { uint32_t lbase = g_bg2d_layer_base + layer * g_bg2d_layer_struct_stride; - if ((int8_t)psx_read_byte(lbase + 0x52) >= 0) continue; int sx = (int16_t)psx_read_half(lbase + 0xa); int sy = (int16_t)psx_read_half(lbase + 0xe); + int8_t parent = (int8_t)psx_read_byte(lbase + 0x52); + if (parent >= 0 && (uint32_t)(uint8_t)parent < g_bg2d_layer_count) { + uint32_t pbase = g_bg2d_layer_base + + (uint32_t)(uint8_t)parent * g_bg2d_layer_struct_stride; + sx += (int16_t)psx_read_half(pbase + 0xa); + sy += (int16_t)psx_read_half(pbase + 0xe); + } for (int ci = 0; ci < g_bg2d_native_cols; ci++) - bg2d_fill_column((int)layer, sx + ci * 16, sy - 0x10, 0, &total, &bad); + bg2d_fill_column((int)layer, sx + ci * 16, sy - 0x10, + sx, 0, &total, &bad); } if (bad_out) *bad_out = bad; return total; @@ -1183,6 +1224,25 @@ static int32_t ws_disp_w(void) { return di.width ? (int32_t)di.width : 320; } +static int32_t ws_disp_h(void) { + GpuDisplayInfo di; + gpu_get_display_info(&di); + return di.height ? (int32_t)di.height : 240; +} + +/* Full-screen fades and environmental filters are authored as 320x240 TILEs. + * In native-wide mode, grow only primitives that cover the complete native + * display; ordinary world-space rectangles remain untouched. */ +static void ws_expand_fullscreen_rect(int32_t *x, int32_t y, int *w, int h) { + if (!ws_native_wide_active()) return; + int W = (int)ws_disp_w(), H = (int)ws_disp_h(); + if (*x <= 0 && *x + *w >= W && y <= 0 && y + h >= H) { + int off = ws_nw_offset(); + *x -= off; + *w += 2 * off; + } +} + /* In-game HUD pivot for an untagged screen-space SPRT spanning [x, x+w). * Only reached during gameplay (ws_active true). HUD elements are sparse and * corner-anchored, so pivot by thirds: outer-third elements anchor to their @@ -1954,6 +2014,28 @@ static void gp0_exec_mono_quad(void) { int32_t vx[4], vy[4]; for (int i = 0; i < 4; i++) parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); + + /* Full-screen filters are commonly encoded as an axis-aligned quad. Drawing + * a semi-transparent quad as two independent triangles blends their shared + * diagonal twice; render the equivalent rectangle once to avoid that seam. + * The full-screen helper also grows the native 320-wide filter across the + * sidecar surface in native-wide mode. Ordinary world quads are unchanged. */ + if (vx[0] == vx[2] && vx[1] == vx[3] && + vy[0] == vy[1] && vy[2] == vy[3] && + vx[1] > vx[0] && vy[2] > vy[0] && + vx[0] <= 0 && vx[1] >= ws_disp_w() && + vy[0] <= 0 && vy[2] >= ws_disp_h()) { + int32_t x = vx[0]; + int32_t y = vy[0]; + int w = (int)(vx[1] - vx[0]); + int h = (int)(vy[2] - vy[0]); + ws_expand_fullscreen_rect(&x, y, &w, h); + x += draw_offset_x; + y += draw_offset_y; + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_rect(x, y, w, h, color); + return; + } ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (no-op else) */ for (int i = 0; i < 4; i++) { vx[i] += draw_offset_x; @@ -2286,6 +2368,7 @@ static void gp0_exec_mono_rect(void) { int h = (gp0_cmd_buf[2] >> 16) & 0xFFFFu; if (w > 1023) w = 1023; if (h > 511) h = 511; + ws_expand_fullscreen_rect(&x0, y0, &w, h); x0 += draw_offset_x; y0 += draw_offset_y; gr_set_semi_transparency(semi_trans, (int)semi_transparency); gr_draw_flat_rect(x0, y0, w, h, color); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 12b4239f..13ac3fd8 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2379,7 +2379,8 @@ int main(int argc, char** argv) { gc.ws_bg2d_layer_stride_addr, gc.ws_bg2d_ring_cols, gc.ws_bg2d_layer_count, - gc.ws_bg2d_layer_struct_stride); + gc.ws_bg2d_layer_struct_stride, + gc.ws_bg2d_packet_cap); /* [widescreen] gte_game_mode — 3D-title gameplay detector (Ape). */ gpu_ws_set_gte_game_mode(gc.ws_gte_game_mode ? 1 : 0); /* [widescreen] nw_hud_corners — push HUD to the true wide corners. */