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