Recover saturated winding at guarded native-wide rendering branches

This commit is contained in:
Matthew Stanley
2026-10-02 16:47:59 -07:00
parent 3787120488
commit 1ed0dfd22d
17 changed files with 200 additions and 5 deletions
+24
View File
@@ -33,3 +33,27 @@ ordinary conservative geometry consumer continues to reject it.
This policy cannot recover a projection whose provenance has already been lost
through unsupported CPU arithmetic, or repair a guest culling decision made
before GPU submission. Those paths still need title-specific qualification.
## Saturation-aware winding consumers
A title can separately bind exact branch words with
`psx_mod_set_native_wide_nclip_sites(addresses, expected, count)` and list its
BLEZ, BGTZ or BGEZ consumers under `[widescreen.cull] nclip_exact_sites`.
Enable `[widescreen] precise_nclip = true` to collect checked GTE projections.
No sites are bound by default. Architectural NCLIP MAC0 and FLAG remain stock;
only the selected rendering branch consumes the corrected winding.
This narrower predicate requires a matching native MAC0, live projection
generation, matching packed words and integer Y, positive depth outside the
GTE near region, bounded precision coordinates, and an X projection that
exceeds its corresponding saturation rail. It leaves ordinary subpixel winding
unchanged. Zero reveal, stale/missing precision, altered branch words, other
addresses, speculative execution and timeline invalidation retain the native
decision. Both triangle rejection and the quad's winding keep branches need
to be identified from the title's renderer.
Generated functions, cached overlay callbacks and the interpreter use the same
full-word-bound host predicate. Overlay callback ABI v27 and codegen version 14
prevent older cached modules from hiding the new forwarding surface.
The `ws_nw` diagnostic also reports `nclip_rescues`, the number of branch
decisions corrected since the title last bound its sites.
+8
View File
@@ -821,6 +821,13 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
// sweeps data-as-code into a function; emitting the hardware decode keeps
// the regen alive AND matches the oracle if the word is ever executed.
check_explicit_branch_site(addr, instr);
if (config_.ws_cull_nclip_exact_sites.count(addr) &&
(opcode == 0x06 || opcode == 0x07 ||
(opcode == 0x01 && ((instr >> 16) & 31) == 1))) {
const char* compare = opcode == 0x06 ? "<= 0" : opcode == 0x07 ? "> 0" : ">= 0";
return fmt::format("psx_ws_nclip_branch(0x{:08X}u, 0x{:08X}u, (int32_t){}, ((int32_t){} {})) /* guarded wide nclip */",
addr, instr, reg_name(rs), reg_name(rs), compare);
}
if (opcode == 0x01) {
uint32_t regimm_op = (instr >> 16) & 0x1F;
if ((regimm_op & 0x01u) == 0x00u) { // bltz family (incl. bltzal + undefined mirrors)
@@ -3450,6 +3457,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
ss << "extern int psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
ss << "extern int psx_ws_masked_reject(uint32_t flags, uint32_t mask); /* guarded packed-coordinate reject */\n";
ss << "extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);\n";
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
ss << "extern int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func, uint32_t counter_addr, uint32_t gpustat_ptr_addr, uint32_t timer1_ptr_addr, uint32_t timer1_cache_addr); /* load_accel.c */\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
@@ -822,6 +822,12 @@ void codegen_tests() {
std::string::npos &&
nclip_exact.find("ws exact nclip") != std::string::npos,
"codegen emits title-scoped exact NCLIP predicate");
for (uint32_t word : {0x18400002u, 0x1C400002u, 0x04410002u}) {
const std::string guarded = generate_first_instruction(word, {}, false, nclip_exact_config);
check(guarded.find("psx_ws_nclip_branch(0x80010000u,") != std::string::npos &&
guarded.find("guarded wide nclip") != std::string::npos,
"inclusive/keep NCLIP branches carry full-word identity to the host");
}
PSXRecomp::CodeGenConfig plane_nx_config;
plane_nx_config.ws_cull_plane_nx_sites.insert(0x80010000u);
+2
View File
@@ -239,6 +239,8 @@ extern void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks,
* only when it belongs to the supplied native MAC0; otherwise preserves the
* native comparison. This does not change guest-visible GTE state. */
extern int gte_nclip_precise_bltz(int32_t native_mac0);
extern int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign);
extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);
/* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2).
* The emitter writes them unconditionally; they expand to real calls only
+2
View File
@@ -339,6 +339,8 @@ void gpu_ws_set_masked_reject_sites(const uint32_t *addresses,
int psx_ws_masked_reject(uint32_t flags, uint32_t mask);
int psx_ws_masked_reject_site(uint32_t pc, uint32_t instr,
uint32_t flags, int vanilla);
int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);
uint64_t gpu_ws_native_wide_nclip_rescues(void);
int psx_ws_is_cull_clip_edge_x_load_site(uint32_t pc);
uint32_t psx_ws_clip_edge_width(void);
uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced);
+5
View File
@@ -113,6 +113,11 @@ int32_t psx_mod_widescreen_x_margin(void);
* native-wide gameplay. Requires exact packet-address/word provenance and
* depth; never changes guest SXY, vertical coordinates, or the 4:3 path. */
void psx_mod_set_native_wide_projection_correction(int enabled);
/* Bind render-only NCLIP branch consumers to full instruction words. These
* recover winding only when valid horizontal projections saturated; guest
* MAC0 and flags remain architectural. Empty registration disables the sites. */
void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses,
const uint32_t* expected, int count);
/* Mark a guest GPU packet (P_TAG address) as persistent screen-space HUD.
* edge = -1 left, +1 right, 0 clears a reused packet's tag. The native-wide
+5 -2
View File
@@ -116,7 +116,8 @@
* retaining the v24 last_store_pc pointer. */
/* v26: guarded packed-coordinate rejection callback, preserving the selected
* rejection axes while the native-wide renderer clips horizontal margins. */
#define PSX_OVERLAY_ABI_VERSION 26
/* v27: full-word-bound native-wide winding branch callback. */
#define PSX_OVERLAY_ABI_VERSION 27
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
* record consumes one slot, even when another DLL carries an identical
@@ -183,7 +184,8 @@
* so packaged pairs cannot be hidden by an older user cache on first boot. */
/* 13: merge adaptive backdrop v12: independent quotient consumers, proven
* copied/biased camera inputs, and two biased bounds for terrain strips. */
#define PSX_OVERLAY_CODEGEN_VER 13
/* 14: guarded saturation-aware NCLIP branch consumers and ABI v27 forwarding. */
#define PSX_OVERLAY_CODEGEN_VER 14
/* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen
* sources). Folded into the cache PATH next to cg<N> (gcc/<arch-abi>/cg<N>_<hash>/)
@@ -360,6 +362,7 @@ typedef struct OverlayCallbacks {
* do. NULL leaves the DLL writing a private copy nothing reads. */
uint32_t *last_store_pc;
int (*ws_masked_reject)(uint32_t flags, uint32_t mask);
int (*ws_nclip_branch)(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);
} OverlayCallbacks;
#ifdef __cplusplus
@@ -268,6 +268,9 @@ uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced) {
int psx_ws_masked_reject(uint32_t flags, uint32_t mask) {
return g_cbs.ws_masked_reject ? g_cbs.ws_masked_reject(flags, mask) : flags != 0u;
}
int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla) {
return g_cbs.ws_nclip_branch ? g_cbs.ws_nclip_branch(pc, instr, mac0, vanilla) : vanilla;
}
uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla,
uint32_t object, int32_t x, int32_t z,
int32_t y) {
+3 -2
View File
@@ -8081,9 +8081,10 @@ static void handle_ws_nw(int id, const char *json)
GpuWsDebug ws;
gpu_ws_get_debug(&ws);
send_fmt("{\"id\":%d,\"ok\":true,\"native_wide\":%d,\"mode\":%d,\"nw_extra\":%d,"
"\"projection_correction\":%d,\"projection_vertices\":%llu}",
"\"projection_correction\":%d,\"projection_vertices\":%llu,\"nclip_rescues\":%llu}",
id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra,
correction, (unsigned long long)corrected_vertices);
correction, (unsigned long long)corrected_vertices,
(unsigned long long)gpu_ws_native_wide_nclip_rescues());
}
/* Live scanline post-process toggle (A/B): `scanline on=<0|1> pct=<0..100>`.
+3
View File
@@ -1986,6 +1986,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
}
case 0x06: { /* BLEZ */
int taken = ws_branch_keep(pc, (int32_t)cpu->gpr[rs] <= 0);
taken = psx_ws_nclip_branch(pc, insn, (int32_t)cpu->gpr[rs], taken);
exec_delay_slot(cpu, pc + 4);
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
@@ -1993,6 +1994,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
}
case 0x07: { /* BGTZ */
int taken = ws_branch_keep(pc, (int32_t)cpu->gpr[rs] > 0);
taken = psx_ws_nclip_branch(pc, insn, (int32_t)cpu->gpr[rs], taken);
exec_delay_slot(cpu, pc + 4);
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
@@ -2026,6 +2028,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
default: return abort_unsupported(pc, insn, "REGIMM rt");
}
taken = ws_branch_keep(pc, taken);
taken = psx_ws_nclip_branch(pc, insn, (int32_t)cpu->gpr[rs], taken);
exec_delay_slot(cpu, pc + 4);
cosim_exec_one_transfer_hook(pc + 4);
cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8);
+34 -1
View File
@@ -274,7 +274,7 @@ void gpu_ws_set_precise_nclip(int on) {
* PGXP correction is disabled. Re-derive the internal transport arm. */
gpu_pgxp_rederive_enable();
}
int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && ws_active(); }
int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && (ws_active() || ws_nw_extra() > 0); }
void gpu_ws_set_gameplay_state_gate(uint32_t addr,
const uint32_t *values, int nvalues) {
if (nvalues < 0) nvalues = 0;
@@ -883,6 +883,39 @@ int psx_ws_masked_reject_site(uint32_t pc, uint32_t instr,
return vanilla;
}
static WsMaskedRejectSite ws_nclip_sites[WS_EXPLICIT_CULL_SITES_MAX];
static int ws_nclip_site_count;
static uint64_t ws_nclip_rescues;
uint64_t gpu_ws_native_wide_nclip_rescues(void) { return ws_nclip_rescues; }
void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses,
const uint32_t* expected, int count) {
if (count < 0 || count > WS_EXPLICIT_CULL_SITES_MAX) abort();
for (int i = 0; i < count; ++i) {
const uint32_t op = expected[i] >> 26, rt = (expected[i] >> 16) & 31;
if (!((op == 1 && rt <= 1) || ((op == 6 || op == 7) && rt == 0))) abort();
ws_nclip_sites[i].address = addresses[i] & 0x1FFFFFFFu;
ws_nclip_sites[i].expected = expected[i];
}
ws_nclip_site_count = count;
ws_nclip_rescues = 0;
}
extern int gte_nclip_native_wide_sign(int32_t mac0, int* sign);
int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla) {
if (psx_ws_x_margin() <= 0 || !gpu_ws_precise_nclip_enabled()) return vanilla;
for (int i = 0; i < ws_nclip_site_count; ++i) {
if (ws_nclip_sites[i].address != (pc & 0x1FFFFFFFu) ||
ws_nclip_sites[i].expected != instr) continue;
int sign;
if (!gte_nclip_native_wide_sign(mac0, &sign)) return vanilla;
const uint32_t op = instr >> 26;
const int result = op == 6 ? sign <= 0 : op == 7 ? sign > 0 :
((instr >> 16) & 1) ? sign >= 0 : sign < 0;
if (result != vanilla) ++ws_nclip_rescues;
return result;
}
return vanilla;
}
typedef struct {
uint32_t address;
uint32_t expected;
+26
View File
@@ -940,6 +940,8 @@ static uint64_t s_nclip_disagreements = 0;
static int32_t s_nclip_last_native = 0;
static int8_t s_nclip_last_precise_sign = 0;
static bool s_nclip_last_precise_valid = false;
static bool s_nclip_last_horizontal_saturated = false;
static uint32_t s_nclip_last_generation;
extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks,
uint64_t *disagreements) {
if (hits) *hits = s_nclip_precise_hits;
@@ -951,6 +953,13 @@ extern "C" int gte_nclip_precise_bltz(int32_t native_mac0) {
return native_mac0 < 0;
return s_nclip_last_precise_sign < 0;
}
extern "C" int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign) {
if (!s_nclip_last_precise_valid || !s_nclip_last_horizontal_saturated ||
native_mac0 != s_nclip_last_native || s_nclip_last_generation != s_geom_generation ||
s_gte_replay_sandbox || s_speculative_depth != 0) return 0;
*sign = s_nclip_last_precise_sign;
return 1;
}
void gte_nclip(GTEState* gte, uint32_t instr) {
gte->FLAG = 0;
@@ -967,6 +976,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) {
int32_t out = static_cast<int32_t>(mac0);
s_nclip_last_native = out;
s_nclip_last_precise_valid = false;
s_nclip_last_horizontal_saturated = false;
/* Compute an exact 16.16 determinant for configured branch consumers, but
* preserve native guest-visible MAC0 for every architectural reader. */
int32_t px0, py0, px1, py1, px2, py2;
@@ -983,6 +993,22 @@ void gte_nclip(GTEState* gte, uint32_t instr) {
const int64_t cross = dx10 * dy20 - dy10 * dx20;
s_nclip_last_precise_sign = cross < 0 ? -1 : (cross > 0 ? 1 : 0);
s_nclip_last_precise_valid = true;
s_nclip_last_generation = s_geom_generation;
/* Narrow rescue: exact Y still agrees with the architectural FIFO,
* depth is outside the GTE near region, and at least one X exceeded
* the matching saturation rail. Ordinary subpixel winding stays stock. */
const int raw_x[3] = {sx0, sx1, sx2}, raw_y[3] = {sy0, sy1, sy2};
const int32_t exact_x[3] = {px0, px1, px2}, exact_y[3] = {py0, py1, py2};
bool checked = true, saturated = false;
for (int i = 0; i < 3; ++i) {
const int x = exact_x[i] >> 16;
checked &= (exact_y[i] >> 16) == raw_y[i] && x > -4096 && x < 4096;
checked &= gte->SZ[i + 1] >= gte->H / 2 && gte->SZ[i + 1] != 0;
if (raw_x[i] == 1023 && x > 1023) saturated = true;
else if (raw_x[i] == -1024 && x < -1024) saturated = true;
else checked &= x == raw_x[i];
}
s_nclip_last_horizontal_saturated = checked && saturated;
if ((cross > 0 && out <= 0) || (cross < 0 && out >= 0))
s_nclip_disagreements++;
} else if (gpu_ws_precise_nclip_enabled() &&
+2
View File
@@ -2629,6 +2629,8 @@ static void init_callbacks(void) {
{
extern int psx_ws_masked_reject(uint32_t flags, uint32_t mask);
s_callbacks.ws_masked_reject = psx_ws_masked_reject;
extern int psx_ws_nclip_branch(uint32_t, uint32_t, int32_t, int);
s_callbacks.ws_nclip_branch = psx_ws_nclip_branch;
}
{
extern uint32_t psx_ws_aspect_cone_result(
+3
View File
@@ -335,3 +335,6 @@ int gr_render_wide_display(uint32_t *out, int pitch, int base_x,
(void)out; (void)pitch; (void)base_x; (void)disp_y; (void)disp_h;
return 0;
}
int gte_nclip_native_wide_sign(int32_t mac0, int* sign) {
(void)mac0; (void)sign; return 0;
}
@@ -625,6 +625,39 @@ int test_precise_nclip_is_title_scoped() {
return 0;
}
int test_saturated_nclip_keeps_architectural_result() {
CPUState cpu{};
gte_precision_tracking_set(1);
g_test_precise_nclip_enabled = 1;
cpu.gte_ctrl[26] = 300;
const int px[] = {1100, 1200, 1300}, py[] = {100, 200, 400};
for (unsigned i=0;i<3;i++) {
uint32_t packed = 1023u | ((uint32_t)py[i] << 16);
cpu.gte_data[12+i] = packed; cpu.gte_data[17+i] = 1000;
gte_test_seed_precise_projection(i, packed, px[i]*65536, py[i]*65536, 1000);
}
gte_execute(&cpu, 6);
int sign = 0;
if (cpu.gte_data[24] != 0 || !gte_nclip_native_wide_sign(0, &sign) || sign != 1 ||
gte_nclip_native_wide_sign(1, &sign))
return fail_value("saturated winding rescue preserves native zero MAC0",0,6,0,0,cpu.gte_data[24]);
cpu.gte_data[17] = 10;
gte_execute(&cpu, 6);
if (gte_nclip_native_wide_sign(0, &sign) || cpu.gte_data[24] != 0)
return fail_value("near depth retains native reject",0,6,0,0,cpu.gte_data[24]);
cpu.gte_data[17] = 1000;
gte_execute(&cpu, 6);
gte_precision_timeline_invalidate();
if (gte_nclip_native_wide_sign(0, &sign))
return fail_value("timeline invalidation clears winding rescue",0,6,0,0,1);
gte_test_seed_precise_projection(0, cpu.gte_data[12] ^ 1u, px[0]*65536, py[0]*65536, 1000);
gte_execute(&cpu, 6);
if (gte_nclip_native_wide_sign(0, &sign))
return fail_value("stale NCLIP projection refuses rescue",0,6,0,0,1);
g_test_precise_nclip_enabled = 0;
return 0;
}
int test_precision_speculative_transaction() {
constexpr uint32_t address = 0x00123450u;
constexpr uint32_t packed = 0x00420021u;
@@ -739,6 +772,7 @@ int main() {
if (int rc = test_command_timing_hook()) return rc;
if (int rc = test_precise_sxy_invalidation()) return rc;
if (int rc = test_precise_nclip_is_title_scoped()) return rc;
if (int rc = test_saturated_nclip_keeps_architectural_result()) return rc;
if (int rc = test_precision_speculative_transaction()) return rc;
std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle");
return 0;
+32
View File
@@ -68,6 +68,10 @@ uint32_t psx_cyc_lwc2_read(CPUState *cpu, uint32_t a) { (void)cpu; return psx_re
int dirty_ram_is_dirty(uint32_t phys) { (void)phys; return 1; }
int mdec_recently_active(uint32_t f) { (void)f; return 0; }
void gte_execute(CPUState *cpu, uint32_t cmd) { (void)cpu; (void)cmd; }
static int precise_sign_valid, precise_sign;
int gte_nclip_native_wide_sign(int32_t mac0, int* sign) {
(void)mac0; *sign = precise_sign; return precise_sign_valid;
}
void gte_precision_store_word(uint32_t a, uint8_t r) { (void)a; (void)r; }
uint32_t gte_read_ctrl(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; }
uint32_t gte_read_data(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; }
@@ -413,12 +417,40 @@ static void test_masked_reject(void) {
gpu_ws_set_margin_override(-1);
}
static void test_wide_nclip_branches(void) {
CPUState cpu = {0};
ws_xnum = 3; ws_xden = 4; ws_mode = 0;
ws_precise_nclip_cfg = 1;
precise_sign_valid = 1; precise_sign = 1;
const uint32_t sites[] = {CODE+0x300,CODE+0x340,CODE+0x380};
const uint32_t words[] = {i_type(6,A0,0,3),i_type(7,A0,0,3),i_type(1,A0,1,3)};
psx_mod_set_native_wide_nclip_sites(sites,words,3);
for (int margin=0;margin<=53;margin+=53) {
gpu_ws_set_margin_override(margin);
cpu.gpr[A0]=0;
CHECK(branch_taken(&cpu,sites[0],words[0]) == (margin==0), "BLEZ native zero vs precise positive");
CHECK(branch_taken(&cpu,sites[1],words[1]) == (margin!=0), "BGTZ native zero vs precise positive");
CHECK(cpu.gpr[A0]==0, "winding helper leaves guest MAC0 register alone");
cpu.gpr[A0]=(uint32_t)-1;
CHECK(branch_taken(&cpu,sites[2],words[2]) == (margin!=0), "BGEZ exact keep predicate");
CHECK(branch_taken(&cpu,sites[0]+4,words[0]), "unlisted twin stays native");
cpu.gpr[V0]=(uint32_t)-1;
CHECK(branch_taken(&cpu,sites[0],i_type(6,V0,0,3)), "wrong full word stays native");
}
precise_sign_valid=0; gpu_ws_set_margin_override(53); cpu.gpr[A0]=0;
CHECK(branch_taken(&cpu,sites[0],words[0]), "missing precision stays native");
psx_mod_set_native_wide_nclip_sites(NULL,NULL,0);
ws_precise_nclip_cfg=0; ws_xnum=ws_xden=1;
gpu_ws_set_margin_override(-1);
}
int main(int argc, char **argv) {
if (argc > 1) s_capture_path = argv[1];
test_store();
test_helpers();
test_interpreter();
test_masked_reject();
test_wide_nclip_branches();
if (failures) {
fprintf(stderr, "%d check(s) failed\n", failures);
return 1;
@@ -22,6 +22,10 @@ static uint32_t seen_address, cycles;
static int32_t bound(int32_t x) { return x * 2; }
static int margin120(void) { return 120; }
static int packed_reject(uint32_t flags, uint32_t mask) { return (flags & mask) != 0; }
static int nclip_branch(uint32_t pc, uint32_t word, int32_t mac0, int vanilla) {
assert(pc == 0x80021EAC && word == 0x1900FFCF && mac0 == 0 && vanilla == 1);
return 0;
}
static void advance(uint32_t n) { cycles += n; }
static int entry(CPUState *cpu, uint32_t address) {
assert(cycles == 17); seen_cpu = cpu; seen_address = address;
@@ -66,6 +70,10 @@ int main(void) {
assert(psx_ws_masked_reject(0xFE00u, 0xFFFF0000u) == 0);
assert(psx_ws_masked_reject(0xFF00FE00u, 0xFFFF0000u) == 1);
assert(psx_ws_masked_reject(0u, 0xFFFF0000u) == 0);
assert(psx_ws_nclip_branch(0x80021EAC, 0x1900FFCF, 0, 1) == 1);
callbacks.ws_nclip_branch = nclip_branch;
overlay_init(&callbacks);
assert(psx_ws_nclip_branch(0x80021EAC, 0x1900FFCF, 0, 1) == 0);
return 0;
}
'''