Merge branch 'master' into feat/mmx5-widescreen-fw

# Conflicts:
#	recompiler/src/config_loader.cpp
#	recompiler/src/config_loader.h
#	recompiler/src/main_psx.cpp
#	runtime/src/gpu.c
#	runtime/src/main.cpp
This commit is contained in:
Matthew Stanley
2026-07-10 08:52:29 -07:00
222 changed files with 12275 additions and 8502 deletions
+131 -22
View File
@@ -5,6 +5,7 @@
// included via relative path to avoid an include-dir collision (recompiler and
// runtime both ship a gte.h).
#include "../../runtime/include/ws_backdrop_detect.h"
#include "../../runtime/include/ws_cull_detect.h"
#include "../../runtime/include/psx_instr_cost.h" /* single-source CPU cycle cost (shared with interp) */
#include <fmt/format.h>
#include <algorithm>
@@ -699,7 +700,7 @@ std::string CodeGenerator::translate_mtlo(uint32_t instr) {
return fmt::format("cpu->lo = {};", reg_name(rs));
}
std::string CodeGenerator::generate_branch_condition(uint32_t instr) {
std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t addr) {
uint32_t opcode = (instr >> 26) & 0x3F;
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
@@ -708,6 +709,11 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr) {
if (opcode == 0x01) {
uint32_t regimm_op = (instr >> 16) & 0x1F;
if (regimm_op == 0x00) { // bltz
// Classified LEFT-edge funnel bltz (auto_screen_x, signed idioms):
// reject only past the revealed margin. Identity at 4:3 (margin 0).
if (ws_cull_bltz_pcs_.count(addr))
return fmt::format("psx_ws_cull_bltz({}) /* ws auto screen-x cull (left edge) */",
reg_name(rs));
return fmt::format("(int32_t){} < 0", reg_name(rs));
} else if (regimm_op == 0x01) { // bgez
return fmt::format("(int32_t){} >= 0", reg_name(rs));
@@ -861,20 +867,45 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
// than culled at the 320 boundary. Same shape as an explicit range_site but
// applied by signature — no per-address list. (Reached only when the addr is
// not already an explicit cull site, which returns above.)
if (ws_auto_cull_func_ && opcode == 0x0B) { // sltiu
if (ws_auto_cull_func_ && (opcode == 0x0B || opcode == 0x0A)) { // sltiu / slti
uint16_t uimm = get_imm16_u(instr);
if (uimm == 0x140 || uimm == 0x141) {
if (psx_ws_cull_imm_in(uimm, config_.ws_cull_w_imms.data(),
(int)config_.ws_cull_w_imms.size())) {
uint32_t rs = get_rs(instr), rt = get_rt(instr);
// Route through the shared runtime helper psx_ws_cull_sltiu so the
// gcc emit, the sljit JIT, and the interpreter all widen identically.
// It sign-extends SX and shifts by +margin (wide window
// -margin <= SX < imm+margin, both edges); at margin=0 it reduces
// bit-for-bit to the vanilla `SX <u imm` verdict (4:3 byte-identical).
return fmt::format("{} = psx_ws_cull_sltiu({}, {});"
" /* ws auto screen-x cull (both edges) */{}",
if (opcode == 0x0B) {
// Route through the shared runtime helper psx_ws_cull_sltiu so the
// gcc emit, the sljit JIT, and the interpreter all widen identically.
// It sign-extends SX and shifts by +margin (wide window
// -margin <= SX < imm+margin, both edges); at margin=0 it reduces
// bit-for-bit to the vanilla `SX <u imm` verdict (4:3 byte-identical).
return fmt::format("{} = psx_ws_cull_sltiu({}, {});"
" /* ws auto screen-x cull (both edges) */{}",
reg_name(rt), reg_name(rs), (int)uimm, comment);
}
// Signed min/max funnel: `slti v, minSX, W` is the RIGHT edge only
// (the paired left edge is the classified bltz — see
// generate_branch_condition); widen by +margin.
return fmt::format("{} = psx_ws_cull_slti({}, {});"
" /* ws auto screen-x cull (right edge) */{}",
reg_name(rt), reg_name(rs), (int)uimm, comment);
}
}
// Explicit signed right-edge widen site ([widescreen.cull] slti_sites) for
// funnel functions the auto-detector cannot qualify (X-only, no H compare).
if (config_.ws_cull_slti_sites.count(addr)) {
if (opcode == 0x0A) { // slti
uint32_t rs = get_rs(instr), rt = get_rt(instr);
uint16_t uimm = get_imm16_u(instr);
return fmt::format("{} = psx_ws_cull_slti({}, {});"
" /* ws cull slti site (right edge) */{}",
reg_name(rt), reg_name(rs), (int)uimm, comment);
} else if (!config_.overlay_mode) {
fmt::print(stderr, "ERROR: [widescreen.cull] slti site 0x{:08X} is not "
"slti (opcode 0x{:02X})\n", addr, opcode);
std::exit(1);
}
// overlay variant: addr is different code here — fall through to vanilla.
}
// Widescreen backdrop screenX squash ([widescreen.backdrop] x_sites). The
// site is the `sh rt,off(base)` storing a parallax 2D backdrop layer's
// final screen-X; squash the stored value around the screen centre so the
@@ -1483,7 +1514,7 @@ std::string CodeGenerator::translate_basic_block(
// For branch-likely variants, delay slot is conditional
if (block.exit_instr.is_likely) {
ss << config_.indent << "/* delay slot (likely) - conditional execution */\n";
ss << config_.indent << "if (" << generate_branch_condition(block.exit_instr.instruction) << ") {\n";
ss << config_.indent << "if (" << generate_branch_condition(block.exit_instr.instruction, block.exit_instr.address) << ") {\n";
ss << config_.indent << translate_instruction(delay_slot_addr, delay_instr) << "\n";
emit_cosim_instr(delay_slot_addr, config_.indent + config_.indent);
ss << config_.indent << "}\n";
@@ -1493,7 +1524,7 @@ std::string CodeGenerator::translate_basic_block(
// instruction (before the delay slot). If the delay slot
// modifies a condition register we must save the result first.
if (block.exit_instr.type == ControlFlowType::Branch) {
std::string cond = generate_branch_condition(block.exit_instr.instruction);
std::string cond = generate_branch_condition(block.exit_instr.instruction, block.exit_instr.address);
delay_saved_cond = fmt::format("_bc_{:08X}", addr);
ss << config_.indent
<< fmt::format("int {} = ({}); /* save branch cond before delay slot */\n",
@@ -1528,7 +1559,7 @@ std::string CodeGenerator::translate_basic_block(
}
// Conditional branch - use pre-captured condition if available
std::string condition = delay_saved_cond.empty()
? generate_branch_condition(block.exit_instr.instruction)
? generate_branch_condition(block.exit_instr.instruction, block.exit_instr.address)
: delay_saved_cond;
if (block.successors.size() == 2) {
ss << config_.indent << "if (" << condition << ") {\n";
@@ -1945,10 +1976,11 @@ std::string CodeGenerator::translate_basic_block(
}
bool CodeGenerator::func_has_screen_extent_cull(const ControlFlowGraph& cfg) const {
// The GTE per-vertex trivial-reject pairs a width compare (sltiu …,0x140 or
// inclusive 0x141) with a height compare (sltiu …,0xE0 or 0xF1) in the same
// function. Presence of both is the signature of a screen-extent render
// funnel; a lone width compare elsewhere (rare) is left alone.
// The GTE trivial-reject pairs a width compare (slti/sltiu, immediate from
// the per-game W set) with a height compare (immediate from the H set) in
// the same function. Presence of both is the signature of a screen-extent
// render funnel; a lone width compare elsewhere (rare) is left alone.
// Shared scan (ws_cull_detect.h) so every backend derives one verdict.
bool has_x = false, has_y = false;
for (uint32_t block_addr : cfg.block_order) {
const BasicBlock& block = cfg.blocks.at(block_addr);
@@ -1956,16 +1988,44 @@ bool CodeGenerator::func_has_screen_extent_cull(const ControlFlowGraph& cfg) con
auto io = exe_.read_word(a);
if (!io.has_value()) continue;
uint32_t in = *io;
if ((in & 0xFC000000u) != 0x2C000000u) continue; // not sltiu
uint16_t im = (uint16_t)(in & 0xFFFF);
if (im == 0x140 || im == 0x141) has_x = true;
else if (im == 0xE0 || im == 0xF1) has_y = true;
uint32_t op = in >> 26;
if (op != 0x0Au && op != 0x0Bu) continue; // not slti/sltiu
uint32_t im = in & 0xFFFFu;
if (psx_ws_cull_imm_in(im, config_.ws_cull_w_imms.data(),
(int)config_.ws_cull_w_imms.size())) has_x = true;
else if (psx_ws_cull_imm_in(im, config_.ws_cull_h_imms.data(),
(int)config_.ws_cull_h_imms.size())) has_y = true;
if (has_x && has_y) return true;
}
}
return false;
}
int CodeGenerator::detect_cull_bltz_sites(const ControlFlowGraph& cfg) {
// Populate ws_cull_bltz_pcs_ with the function's LEFT-edge funnel bltz
// addresses (ws_cull_detect.h idioms 2/3). Only meaningful when the
// function already qualified (ws_auto_cull_func_). Build a contiguous word
// image over the function range (same approach as detect_backdrop_windows)
// so the structural classifier sees cross-block context.
ws_cull_bltz_pcs_.clear();
if (!ws_auto_cull_func_) return 0;
uint32_t lo = cfg.function_start, hi = cfg.function_end;
if (hi <= lo || (hi - lo) > 0x40000u) return 0; // sanity bound
std::vector<uint32_t> words;
words.reserve((hi - lo) / 4u);
for (uint32_t a = lo; a < hi; a += 4) {
auto io = exe_.read_word(a);
words.push_back(io.has_value() ? *io : 0u);
}
int n = (int)words.size();
for (int i = 0; i < n; i++)
if (psx_ws_cull_bltz_here(words.data(), n, i,
config_.ws_cull_w_imms.data(),
(int)config_.ws_cull_w_imms.size()))
ws_cull_bltz_pcs_.insert(lo + (uint32_t)i * 4u);
return (int)ws_cull_bltz_pcs_.size();
}
int CodeGenerator::detect_backdrop_windows(const ControlFlowGraph& cfg) {
// Always clear so a stale map from the previous function can never leak.
ws_backdrop_sites_.clear();
@@ -2003,9 +2063,11 @@ GeneratedFunction CodeGenerator::generate_function(
// Widescreen auto cull (gated): detect the screen-extent reject signature so
// translate_instruction widens this function's width compares. Cleared for
// every function so it never leaks across functions.
// every function so it never leaks across functions. The LEFT-edge funnel
// bltz sites (signed idioms) are classified alongside.
ws_auto_cull_func_ = config_.ws_auto_screen_x_cull &&
func_has_screen_extent_cull(cfg);
detect_cull_bltz_sites(cfg);
// Widescreen backdrop preload (gated): detect each scrolling-backdrop column
// window so translate_instruction rewrites its START/END finalize. Cleared +
@@ -2119,6 +2181,10 @@ GeneratedFunction CodeGenerator::generate_function(
body_ss << config_.indent
<< "psx_ws_sprite_tag(cpu); /* widescreen: record prim ($a0) + anchor */\n";
}
if (config_.ws_bg2d_init_func == func.start_addr) {
body_ss << config_.indent
<< "psx_ws_mmx6_bg_stage_init(); /* widescreen: invalidate stale reveal at stage BG init */\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";
@@ -2247,6 +2313,7 @@ std::vector<GeneratedFunction> CodeGenerator::generate_alias_group(
// generate_function() call can't leak into this body. (See generate_function.)
ws_auto_cull_func_ = config_.ws_auto_screen_x_cull &&
func_has_screen_extent_cull(cfg);
detect_cull_bltz_sites(cfg);
// Backdrop preload sites for this alias group's shared CFG (see
// generate_function). Set per-group so a stale map can't leak in.
@@ -2483,6 +2550,45 @@ std::vector<GeneratedFunction> CodeGenerator::generate_all_functions(
}
GeneratedFunction gen_func = generate_function(func, cfg, fallthrough_name);
// PSX_EMIT_PROD (DEFAULT ON): a "function" whose body is overwhelmingly
// untranslatable (TODO-opcode) words is a data region that discovery's
// primary-opcode-only validity check let slip through as code (valid
// primary opcode, garbage sub-field). Re-emit it as the same compact,
// fail-closed data stub used for is_data_section — if it is ever
// dispatched (it should not be: real reachable code never hits a TODO)
// psx_unknown_dispatch fatals LOUDLY rather than silently no-op'ing.
// This is strictly more faithful than the baseline it replaces (which
// emitted a SILENT no-op for the same words). Smoke-validated on Tomba,
// Tomba2, Ape Escape, MMX6. Set PSX_EMIT_PROD=0 to restore the full-scan
// (oracle) emission. Proper productionization moves this judgment UPSTREAM
// into discovery's validity check (sub-field aware) in function_analysis.
static int s_emit_prod = -1;
if (s_emit_prod < 0) {
const char* e = std::getenv("PSX_EMIT_PROD");
s_emit_prod = (e && *e == '0') ? 0 : 1; // default ON; =0 restores full-scan baseline
}
if (s_emit_prod) {
size_t todo = 0, pos = 0;
while ((pos = gen_func.body.find("TODO: opcode", pos)) != std::string::npos) { todo++; pos += 12; }
pos = 0;
while ((pos = gen_func.body.find("TODO: SPECIAL", pos)) != std::string::npos) { todo++; pos += 13; }
uint32_t instrs = func.size / 4u;
if (instrs > 0 && todo * 2u >= instrs) { // >= 50% untranslatable => data
GeneratedFunction data_stub;
data_stub.function_name = func.name;
data_stub.signature = fmt::format("void {}(CPUState* cpu)", func.name);
data_stub.body = fmt::format(
"{{\n psx_unknown_dispatch(cpu, 0x{:08X}u, 0x{:08X}u);\n}}\n",
func.start_addr, func.start_addr & 0x1FFFFFFFu);
data_stub.full_code = data_stub.signature + "\n" + data_stub.body;
data_stub.line_count = 4;
total_lines += data_stub.line_count;
results.push_back(std::move(data_stub));
continue;
}
}
total_lines += gen_func.line_count;
results.push_back(gen_func);
}
@@ -2514,8 +2620,11 @@ std::string CodeGenerator::generate_file(
ss << "extern void cosim_instr(uint32_t pc);\n";
ss << "#endif\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
ss << "extern void psx_ws_mmx6_bg_stage_init(void); /* ws 2D stage reveal invalidation (gpu.c) */\n";
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_cull_slti(uint32_t sx, uint32_t imm); /* ws cull signed right edge (gpu.c) */\n";
ss << "extern int psx_ws_cull_bltz(uint32_t v); /* ws cull signed left edge (gpu.c) */\n";
ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (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";