From ea8f1308e64edcf32aff206d2c6769bb4736576d Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 01:19:15 -0400 Subject: [PATCH 1/8] CTR graphical diagnostic --- runtime/src/debug_server.c | 514 ++++++++++++++++++++++++++++++++++++- tools/debug_client.py | 23 ++ 2 files changed, 535 insertions(+), 2 deletions(-) diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 1a453e91..2d777945 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -2267,6 +2267,368 @@ static inline void cyc_watch_observe(uint32_t block_leader_phys) if (s_cyc_watch_hits >= s_cyc_watch_max_hits) s_cyc_watch_armed = 0; } +/* ---- pc_probe: multi-PC block-leader counters + reg samples (default-off) ---- + * Arm via TCP `pc_probe_arm` or env PSX_PC_PROBE / PSX_ND_INTRO_PROBE. + * Samples $t0/$fp/$v0/frame at matched leaders. Disarmed = one branch. + * nd_intro=2 also fills mode/depth/ot_base/ot_index from GP + game struct. */ +#define PC_PROBE_MAX_PCS 16 +#define PC_PROBE_SAMPLE_CAP 64 +typedef struct { + uint32_t pc; + uint64_t count; + uint32_t last_t0; + uint32_t last_fp; + uint32_t last_v0; + uint32_t last_mode; + uint32_t last_depth; + uint32_t last_ot_base; + uint32_t last_ot_index; + uint32_t last_frame; + uint32_t t0_zero; + uint32_t t0_nonzero; +} PcProbeSlot; +typedef struct { + uint32_t pc; + uint32_t frame; + uint32_t t0; + uint32_t fp; + uint32_t v0; + uint32_t mode; + uint32_t depth; + uint32_t ot_base; + uint32_t ot_index; +} PcProbeSample; +static volatile int s_pc_probe_armed = 0; +static int s_pc_probe_n = 0; +static PcProbeSlot s_pc_probe[PC_PROBE_MAX_PCS]; +static PcProbeSample s_pc_probe_samples[PC_PROBE_SAMPLE_CAP]; +static uint32_t s_pc_probe_sample_n = 0; +static uint32_t s_pc_probe_sample_max = 32; + +static void pc_probe_clear_state(void) +{ + s_pc_probe_armed = 0; + s_pc_probe_n = 0; + s_pc_probe_sample_n = 0; + memset(s_pc_probe, 0, sizeof(s_pc_probe)); + memset(s_pc_probe_samples, 0, sizeof(s_pc_probe_samples)); +} + +static int pc_probe_add_pc(uint32_t raw) +{ + uint32_t phys = raw & 0x1FFFFFFFu; + if (phys == 0) return 0; + for (int i = 0; i < s_pc_probe_n; i++) { + if (s_pc_probe[i].pc == phys) return 1; + } + if (s_pc_probe_n >= PC_PROBE_MAX_PCS) return 0; + s_pc_probe[s_pc_probe_n].pc = phys; + s_pc_probe_n++; + return 1; +} + +/* Comma/space-separated hex list, e.g. "0x80044D10,0x80044E58". */ +static int pc_probe_parse_list(const char *list) +{ + if (!list || !*list) return 0; + char tmp[512]; + snprintf(tmp, sizeof(tmp), "%s", list); + int added = 0; + for (char *p = tmp, *tok; (tok = strtok(p, ", \t\n")) != NULL; p = NULL) { + uint32_t raw = (uint32_t)strtoul(tok, NULL, 0); + if (pc_probe_add_pc(raw)) added++; + } + return added; +} + +static void pc_probe_arm_nd_intro_defaults(void) +{ + /* NdIntroMeshDraw post-RTPT funnel (block leaders with cyc_observe). */ + static const uint32_t k[] = { + 0x80044580u, /* after dispatch: $fp = v0 OT ptr */ + 0x80044C80u, /* face clip start */ + 0x80044CB0u, /* passed neg outcode */ + 0x80044CDCu, /* passed hi outcode → alloc */ + 0x80044D10u, /* $t0 == 0 ? skip : emit */ + 0x80044D18u, /* emit continue */ + 0x80044DA4u, /* PolyG4 build */ + 0x80044E58u, /* clip skip */ + 0x80044EC8u, /* $t0==0 / OOM skip */ + }; + for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) + pc_probe_add_pc(k[i]); +} + +/* OT/depth leafs of NdIntroDrawDispatch — which path returns the OT slot. */ +static void pc_probe_arm_nd_intro_ot_defaults(void) +{ + static const uint32_t k[] = { + 0x80044580u, /* after jal: v0 → $fp OT slot */ + 0x800440A0u, /* dispatch entry (mode half @ gp+0x4DE) */ + 0x80044120u, /* mode0 depth scale using *(game+0x1D04) */ + 0x80044154u, /* return OT base via *(game+buf*4+0x18C8) */ + 0x800441A8u, /* return *(game+0x25C)+0xFFC (near-ish) */ + 0x800441C0u, /* mode2 entry */ + 0x80044268u, /* mode1/ shared OT-base return */ + 0x80044DA4u, /* PolyG4 emit: confirm $fp sticky */ + }; + for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) + pc_probe_add_pc(k[i]); +} + +/* Textured wood funnel — live path is func_8006A52C (not dead twin 0x8006A6B8). */ +static void pc_probe_arm_nd_intro_wood_defaults(void) +{ + static const uint32_t k[] = { + 0x8006A52Cu, /* NdIntroWoodEmit entry */ + 0x8006A564u, /* RTPT GTE caller_ra (sticky) */ + 0x8006A57Cu, /* post-RTPT face loop */ + 0x8006A590u, /* FLAG read / face setup */ + 0x8006A5B4u, /* NCLIP */ + 0x8006A610u, /* next-face hub (skip + post-emit) */ + 0x8006A69Cu, /* RTPS single-vert return (bgez $0 → loop) */ + 0x8006A6B8u, /* dead twin entry — expect count=0 */ + }; + for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) + pc_probe_add_pc(k[i]); +} + +/* Depth compare: wood OTZ vs digit/glow OT link ptrs vs MeshDraw OtFar. + * Only block-leader PCs (cyc_observe); mid-block AddPrim sites never fire. */ +static void pc_probe_arm_nd_intro_depth_defaults(void) +{ + static const uint32_t k[] = { + 0x8006A608u, /* wood jalr emit — GTE OTZ */ + 0x80023094u, /* DigitGt4 entry — $t7 already OT slot */ + 0x80053E10u, /* glow color ori — $a2=prim; OT via game+0x147C later */ + 0x800440A0u, /* DrawDispatch entry — GP mode/depth */ + 0x80044580u, /* MeshDraw after dispatch */ + 0x800441A8u, /* OtFar leaf */ + 0x80052F98u, /* DigitFx entry */ + 0x800444ECu, /* MeshDraw entry — sample $ra */ + 0x80044DA4u, /* PolyG4 emit hub — lighting bytes on stack */ + }; + for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) + pc_probe_add_pc(k[i]); +} + +/* Wood DL / helper selection: batch setup + which emit helper is bound. */ +static void pc_probe_arm_nd_intro_wood_dl_defaults(void) +{ + static const uint32_t k[] = { + 0x8006AAF0u, /* wood batch setup: a0=stream, ra=material desc */ + 0x8006AB58u, /* fallthrough past flag gates (accepted batch) */ + 0x8006ACE0u, /* after lw s5,96(ra) — helper bound */ + 0x8006A52Cu, /* wood emit entry */ + 0x8006A608u, /* wood jalr emit — also GTE OTZ/SXY via wood_ot */ + 0x8006AE90u, /* alt helper — expect 0 in ND */ + 0x8006AD20u, /* wood batch epilogue / next-opcode hub */ + 0x80044580u, /* MeshDraw post-RTPT — compare SX band */ + }; + for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) + pc_probe_add_pc(k[i]); +} + +/* Resolve double-buffered OT base: *(*(0x8008D2AC) + 0x18C8 + (*(+0xC)<<2)). */ +static void pc_probe_ot_context(CPUState *cpu, uint32_t v0_ot, + uint32_t *mode_out, uint32_t *depth_out, + uint32_t *ot_base_out, uint32_t *ot_index_out) +{ + uint32_t mode = 0, depth = 0, ot_base = 0, ot_index = 0xFFFFFFFFu; + if (cpu) { + uint32_t gp = cpu->gpr[28]; + mode = psx_read_word(gp + 0x4D4u); + /* Depth countdown half lives at gp+0x4D8 (signed). */ + depth = (uint32_t)(int32_t)(int16_t)(psx_read_word(gp + 0x4D8u) & 0xFFFFu); + uint32_t game = psx_read_word(0x8008D2ACu); + if (game) { + uint32_t buf = psx_read_word(game + 0xCu); + ot_base = psx_read_word(game + (buf << 2) + 0x18C8u); + if (ot_base && v0_ot) { + int32_t delta = (int32_t)(v0_ot - ot_base); + if ((delta & 3) == 0 && delta >= 0 && delta < (1 << 20)) + ot_index = (uint32_t)(delta >> 2); + } + } + } + if (mode_out) *mode_out = mode; + if (depth_out) *depth_out = depth; + if (ot_base_out) *ot_base_out = ot_base; + if (ot_index_out) *ot_index_out = ot_index; +} + +static inline void pc_probe_observe(uint32_t block_leader_phys) +{ + if (!s_pc_probe_armed || s_pc_probe_n <= 0) return; + extern CPUState *debug_cpu_ptr; + CPUState *cpu = debug_cpu_ptr; + for (int i = 0; i < s_pc_probe_n; i++) { + if (s_pc_probe[i].pc != block_leader_phys) continue; + PcProbeSlot *s = &s_pc_probe[i]; + s->count++; + if (cpu) { + uint32_t t0 = cpu->gpr[8]; + uint32_t fp = cpu->gpr[30]; + uint32_t v0 = cpu->gpr[2]; + uint32_t a1 = cpu->gpr[5]; + uint32_t a2 = cpu->gpr[6]; + uint32_t t7 = cpu->gpr[15]; + uint32_t otz = cpu->gte_data[7] & 0xFFFFu; + uint32_t mode = 0, depth = 0, ot_base = 0, ot_index = 0xFFFFFFFFu; + /* Wood: a2 is stack-table value (often prim-ish); OT slot ≈ ot_base+OTZ. + * Digit GT4 AddPrim @23180: $t7 = OT slot. Glow @53EB8: $a1 = OT head. */ + int wood_ot = (block_leader_phys == 0x0006A608u || + block_leader_phys == 0x0006A600u); + int digit_ot = (block_leader_phys == 0x00023094u); + int glow_ot = (block_leader_phys == 0x00053E10u); + int dispatch = (block_leader_phys == 0x000440A0u); + uint32_t ot_ptr = 0; + if (digit_ot) + ot_ptr = t7; /* caller-supplied OT slot */ + else if (glow_ot) { + /* OT head at game+0x147C (same as AddPrim a few insns later). */ + uint32_t game = psx_read_word(0x8008D2ACu); + ot_ptr = game ? psx_read_word(game + 0x147Cu) : 0; + } else if (block_leader_phys == 0x00044580u || + block_leader_phys == 0x000441A8u) + ot_ptr = v0; /* dispatch result / OtFar v0 */ + else if (wood_ot) + ot_ptr = 0; /* resolve via OTZ after ot_base known */ + else + ot_ptr = fp ? fp : v0; + pc_probe_ot_context(cpu, ot_ptr, &mode, &depth, &ot_base, &ot_index); + if (wood_ot) { + /* Prefer OTZ as OT index (PsyQ AddPrim(ot+OTZ) style). */ + if (ot_base && otz < (1u << 20)) { + ot_ptr = ot_base + (otz << 2); + ot_index = otz; + } + t0 = a2; + depth = otz; + v0 = otz; + /* Pack max screen-X of SXY0/1/2 into mode for extent compares. */ + { + int32_t sx_max = -0x8000; + for (int si = 12; si <= 14; si++) { + int32_t sx = (int32_t)(int16_t)(cpu->gte_data[si] & 0xFFFFu); + if (sx > sx_max) sx_max = sx; + } + mode = (uint32_t)sx_max; + } + } else if (digit_ot) { + t0 = t7; + v0 = ot_index; + } else if (glow_ot) { + t0 = ot_ptr; + v0 = ot_index; + depth = a2; /* prim ptr */ + } else if (dispatch) { + /* Fresh GP mode/depth before leaf runs; v0 not OT yet. */ + t0 = mode; + v0 = depth; + } else if (block_leader_phys == 0x000444ECu) { + /* MeshDraw entry: expose caller $ra (no jal-site in EXE). */ + t0 = cpu->gpr[31]; + v0 = cpu->gpr[2]; /* entry gate */ + } else if (block_leader_phys == 0x00044DA4u) { + /* PolyG4 emit: sp+0x40/0x41 hold computed shade bytes. */ + uint32_t sp = cpu->gpr[29]; + t0 = psx_read_word(sp + 0x40u) & 0xFFFFu; + v0 = fp; /* OT link ptr */ + } else if (block_leader_phys == 0x0006A52Cu) { + /* Wood emit entry: $t9=face list, $at=ctx, $s5=emit helper. */ + t0 = cpu->gpr[25]; /* t9 */ + v0 = cpu->gpr[1]; /* at */ + depth = cpu->gpr[21]; /* s5 */ + } else if (block_leader_phys == 0x0006ACE0u) { + /* Post helper-select: $s5 set, $a2=model. + * ra may be clobbered by pre-emit jalr; trust s5 + model. */ + t0 = cpu->gpr[21]; /* s5 helper */ + v0 = cpu->gpr[6]; /* a2 model */ + if (v0) { + mode = psx_read_word(v0 + 184u); /* flags */ + /* face-list ptr @a2+200; depth = first face word */ + uint32_t faces = psx_read_word(v0 + 200u); + depth = faces ? psx_read_word(faces) : 0; + ot_base = faces; + ot_index = faces ? psx_read_word(faces + 4u) : 0; + } + } else if (block_leader_phys == 0x0006AAF0u) { + /* Wood batch setup: $a0 stream, $ra = model (helpers at +92). + * mode=flags@+184, depth=helper@+96, ot_base=name word@+8. */ + t0 = cpu->gpr[4]; /* a0 */ + v0 = cpu->gpr[31]; /* model */ + if (v0) { + mode = psx_read_word(v0 + 184u); /* draw flags */ + depth = psx_read_word(v0 + 96u); /* s5 helper */ + ot_base = psx_read_word(v0 + 8u); /* name/tag */ + ot_index = psx_read_word(v0 + 92u); /* pre-emit */ + } + } else if (block_leader_phys == 0x0006AB58u) { + /* Fallthrough past flag bne — batch accepted. */ + t0 = cpu->gpr[6]; /* a2 */ + v0 = t0 ? psx_read_word(t0 + 184u) : 0; + depth = cpu->gpr[31]; /* desc */ + } else if (block_leader_phys == 0x00044580u) { + /* MeshDraw post-dispatch: also pack GTE max SX into mode. */ + { + int32_t sx_max = -0x8000; + for (int si = 12; si <= 14; si++) { + int32_t sx = (int32_t)(int16_t)(cpu->gte_data[si] & 0xFFFFu); + if (sx > sx_max) sx_max = sx; + } + mode = (uint32_t)sx_max; + } + } else if (block_leader_phys == 0x000444E8u) { + /* True MeshDraw entry: lh gp+0x4DC → v0 gate (observe pre-lh). */ + t0 = cpu->gpr[28]; /* gp */ + v0 = cpu->gpr[2]; + } else if (block_leader_phys == 0x00069BB0u) { + /* Sibling matrix/OT setup entry: a1=ctx (s4+360), a0=mesh. */ + t0 = cpu->gpr[5]; /* a1 */ + v0 = cpu->gpr[4]; /* a0 */ + if (t0) { + mode = psx_read_word(t0 + 244u); /* OT base field before +4092 */ + depth = mode ? (mode + 4092u) : 0; + ot_base = mode; + } + } else if (block_leader_phys == 0x00069C34u || + block_leader_phys == 0x00069CC4u) { + /* Face-loop jal / entry: a3 = OT origin (last slot). */ + t0 = cpu->gpr[7]; /* a3 */ + v0 = cpu->gpr[5]; /* a1 */ + mode = cpu->gpr[4]; /* a0 mesh */ + depth = t0; + ot_base = (t0 >= 4092u) ? (t0 - 4092u) : 0; + } + s->last_t0 = t0; + s->last_fp = fp; + s->last_v0 = v0; + s->last_mode = mode; + s->last_depth = depth; + s->last_ot_base = ot_base; + s->last_ot_index = ot_index; + s->last_frame = (uint32_t)s_frame_count; + if (t0 == 0) s->t0_zero++; + else s->t0_nonzero++; + if (s_pc_probe_sample_n < s_pc_probe_sample_max && + s_pc_probe_sample_n < PC_PROBE_SAMPLE_CAP) { + PcProbeSample *sm = &s_pc_probe_samples[s_pc_probe_sample_n++]; + sm->pc = block_leader_phys; + sm->frame = (uint32_t)s_frame_count; + sm->t0 = t0; + sm->fp = fp; + sm->v0 = v0; + sm->mode = mode; + sm->depth = depth; + sm->ot_base = ot_base; + sm->ot_index = ot_index; + } + } + return; + } +} + /* Exported per-basic-block-leader cycle observer. Emitted by the recompiler at * EVERY compiled block leader (under #ifndef PSX_NO_DEBUG_TOOLS, so prod builds * emit nothing — zero overhead) so native's cycle observation matches Beetle's @@ -2280,11 +2642,58 @@ void debug_server_cyc_observe(uint32_t block_leader_phys) { return; #else if (s_fmv_quiet) return; - cyc_watch_observe(block_leader_phys & 0x1FFFFFFFu); + uint32_t phys = block_leader_phys & 0x1FFFFFFFu; + /* ND debug: PSX_ND_WOOD_CLEAR80=1 clears model+184 bit0x80 at the flag-load + * leader so 0x5CF/'cras' models enter NdIntroWoodBatchSetup's textured path. + * Fires before lw v1,184(a2) @ AB3C. Default-off. */ + if (phys == 0x0006AB3Cu) { + static int s_clear80 = -1; + if (s_clear80 < 0) { + const char *e = getenv("PSX_ND_WOOD_CLEAR80"); + s_clear80 = (e && *e && *e != '0') ? 1 : 0; + if (s_clear80) + fprintf(stdout, "psxrecomp: PSX_ND_WOOD_CLEAR80 enabled\n"); + } + if (s_clear80 && debug_cpu_ptr) { + uint32_t a2 = debug_cpu_ptr->gpr[6]; + if (a2) { + uint32_t fl = psx_read_word(a2 + 184u); + if (fl & 0x80u) + psx_write_word(a2 + 184u, fl & ~0x80u); + } + } + } + /* ND debug: PSX_ND_SIB_OT_LIFT= adds n*4 to $a3 at sibling face-loop + * entry 0x80069CC4 (a3 = *(a1+244)+4092 = last OT slot). Sibling PolyFT3 + * right-flap faces use face_hi≈0 → farthest bucket (drawn before additive + * 0x36 glow). Negative n (e.g. -800) moves them nearer in the 1024-entry OT. + * Applied once per a3 value. */ + if (phys == 0x00069CC4u && debug_cpu_ptr) { + static int s_sib_lift = -2; /* -2 unset, 0 disabled, else delta */ + static uint32_t s_sib_lifted_a3 = 0; + if (s_sib_lift == -2) { + const char *e = getenv("PSX_ND_SIB_OT_LIFT"); + if (e && *e) { + s_sib_lift = atoi(e); + fprintf(stdout, "psxrecomp: PSX_ND_SIB_OT_LIFT=%d\n", s_sib_lift); + } else { + s_sib_lift = 0; + } + } + if (s_sib_lift != 0) { + uint32_t a3 = debug_cpu_ptr->gpr[7]; + if (a3 && a3 != s_sib_lifted_a3) { + debug_cpu_ptr->gpr[7] = a3 + (uint32_t)(int32_t)(s_sib_lift * 4); + s_sib_lifted_a3 = debug_cpu_ptr->gpr[7]; + } + } + } + cyc_watch_observe(phys); + pc_probe_observe(phys); /* #2 lockstep comparator: per-basic-block compiled-vs-interp check. Self-gates * on the armed frame window; ~free (one branch) when disarmed. */ { extern void ls_at_leader(uint32_t, CPUState*); extern CPUState *debug_cpu_ptr; - ls_at_leader(block_leader_phys & 0x1FFFFFFFu, debug_cpu_ptr); } + ls_at_leader(phys, debug_cpu_ptr); } #endif } @@ -9034,6 +9443,81 @@ static void handle_cyc_watch_clear(int id, const char *json) send_ok(id); } +/* pc_probe_arm — {"pcs":"0xA,0xB", "n":32, "nd_intro":1..5} or empty pcs+nd_intro. + * 1=PolyG4 clip 2=OT leaves 3=wood 4=depth 5=wood DL/helper bind. */ +static void handle_pc_probe_arm(int id, const char *json) +{ + pc_probe_clear_state(); + int n = json_get_int(json, "n", 32); + if (n < 1) n = 1; + if (n > PC_PROBE_SAMPLE_CAP) n = PC_PROBE_SAMPLE_CAP; + s_pc_probe_sample_max = (uint32_t)n; + + int nd = json_get_int(json, "nd_intro", 0); + char pcs[512]; + const char *have_pcs = json_get_str(json, "pcs", pcs, sizeof(pcs)); + if (nd == 5) pc_probe_arm_nd_intro_wood_dl_defaults(); + else if (nd == 4) pc_probe_arm_nd_intro_depth_defaults(); + else if (nd == 3) pc_probe_arm_nd_intro_wood_defaults(); + else if (nd == 2) pc_probe_arm_nd_intro_ot_defaults(); + else if (nd) pc_probe_arm_nd_intro_defaults(); + if (have_pcs) pc_probe_parse_list(pcs); + if (s_pc_probe_n <= 0) { + send_err(id, "pc_probe_arm needs pcs=... and/or nd_intro=1..5"); + return; + } + s_pc_probe_armed = 1; + send_fmt("{\"id\":%d,\"ok\":true,\"armed\":1,\"n_pcs\":%d,\"sample_max\":%u,\"nd_intro\":%d}", + id, s_pc_probe_n, s_pc_probe_sample_max, nd); +} + +static void handle_pc_probe_dump(int id, const char *json) +{ + (void)json; + char buf[512]; + snprintf(buf, sizeof(buf), + "{\"id\":%d,\"ok\":true,\"armed\":%d,\"n_pcs\":%d,\"sample_max\":%u," + "\"samples_n\":%u,\"slots\":[", + id, s_pc_probe_armed ? 1 : 0, s_pc_probe_n, s_pc_probe_sample_max, + s_pc_probe_sample_n); + send_line(buf); + for (int i = 0; i < s_pc_probe_n; i++) { + const PcProbeSlot *s = &s_pc_probe[i]; + snprintf(buf, sizeof(buf), + "%s{\"pc\":\"0x%08X\",\"count\":%llu,\"t0_zero\":%u,\"t0_nonzero\":%u," + "\"last_t0\":\"0x%08X\",\"last_fp\":\"0x%08X\",\"last_v0\":\"0x%08X\"," + "\"last_mode\":\"0x%08X\",\"last_depth\":\"0x%08X\"," + "\"last_ot_base\":\"0x%08X\",\"last_ot_index\":\"0x%08X\"," + "\"last_frame\":%u}", + (i == 0) ? "" : ",", + s->pc, (unsigned long long)s->count, s->t0_zero, s->t0_nonzero, + s->last_t0, s->last_fp, s->last_v0, + s->last_mode, s->last_depth, s->last_ot_base, s->last_ot_index, + s->last_frame); + send_line(buf); + } + send_line("],\"samples\":["); + for (uint32_t i = 0; i < s_pc_probe_sample_n; i++) { + const PcProbeSample *sm = &s_pc_probe_samples[i]; + snprintf(buf, sizeof(buf), + "%s{\"pc\":\"0x%08X\",\"frame\":%u,\"t0\":\"0x%08X\"," + "\"fp\":\"0x%08X\",\"v0\":\"0x%08X\",\"mode\":\"0x%08X\"," + "\"depth\":\"0x%08X\",\"ot_base\":\"0x%08X\",\"ot_index\":\"0x%08X\"}", + (i == 0) ? "" : ",", + sm->pc, sm->frame, sm->t0, sm->fp, sm->v0, + sm->mode, sm->depth, sm->ot_base, sm->ot_index); + send_line(buf); + } + send_line("]}"); +} + +static void handle_pc_probe_clear(int id, const char *json) +{ + (void)json; + pc_probe_clear_state(); + send_ok(id); +} + /* Liveness/freeze diagnostic: returns a single snapshot of every counter * that distinguishes "stuck in a tight handler loop" from "just slow" or * "starved on TCP poll". Pass {"window":N} (default 256) to also include @@ -12734,6 +13218,9 @@ static const CmdEntry s_commands[] = { { "cyc_watch", handle_cyc_watch }, { "cyc_watch_dump", handle_cyc_watch_dump }, { "cyc_watch_clear", handle_cyc_watch_clear }, + { "pc_probe_arm", handle_pc_probe_arm }, + { "pc_probe_dump", handle_pc_probe_dump }, + { "pc_probe_clear", handle_pc_probe_clear }, { "mmio_dump", handle_mmio_dump }, { "mmio_clear", handle_mmio_clear }, { "capture_freeze", handle_capture_freeze }, @@ -12933,6 +13420,29 @@ void debug_server_init(int port) if (s_rwatch_hi > s_rwatch_lo) g_ram_read_watch_active = 1; } } + /* PSX_ND_INTRO_PROBE=1 PolyG4; =2 OT; =3 wood; =4 depth; =5 wood DL. + * PSX_PC_PROBE="0xA,0xB" arms an explicit list (can combine). */ + { + const char *nd = getenv("PSX_ND_INTRO_PROBE"); + const char *pcs = getenv("PSX_PC_PROBE"); + if ((nd && *nd && *nd != '0') || (pcs && *pcs)) { + pc_probe_clear_state(); + s_pc_probe_sample_max = 48; + if (nd && *nd && *nd != '0') { + int ndv = (int)strtol(nd, NULL, 0); + if (ndv == 5) pc_probe_arm_nd_intro_wood_dl_defaults(); + else if (ndv == 4) pc_probe_arm_nd_intro_depth_defaults(); + else if (ndv == 3) pc_probe_arm_nd_intro_wood_defaults(); + else if (ndv == 2) pc_probe_arm_nd_intro_ot_defaults(); + else pc_probe_arm_nd_intro_defaults(); + } + if (pcs && *pcs) pc_probe_parse_list(pcs); + if (s_pc_probe_n > 0) { + s_pc_probe_armed = 1; + fprintf(stdout, "psxrecomp: pc_probe armed (%d pcs)\n", s_pc_probe_n); + } + } + } } #ifdef _WIN32 diff --git a/tools/debug_client.py b/tools/debug_client.py index f11c8c0a..b6d264cf 100644 --- a/tools/debug_client.py +++ b/tools/debug_client.py @@ -437,6 +437,29 @@ def build_cmd(args): if len(args) > 2: d["nth"] = int(args[2]) return d, pretty_json + elif cmd == "pc_probe_arm": + # pc_probe_arm [nd_intro=1|2|3|nd|nd_ot|nd_wood] [pcs=0xA,0xB] [n=32] + d = {"cmd": "pc_probe_arm"} + for a in args[1:]: + if "=" in a: + k, v = a.split("=", 1) + if k in ("n", "nd_intro") and v.lstrip("-").isdigit(): + d[k] = int(v) + else: + d[k] = v + elif a in ("nd", "nd_intro"): + d["nd_intro"] = 1 + elif a in ("nd_ot", "ot"): + d["nd_intro"] = 2 + elif a in ("nd_wood", "wood"): + d["nd_intro"] = 3 + elif a in ("nd_depth", "depth"): + d["nd_intro"] = 4 + return d, pretty_json + elif cmd == "pc_probe_dump": + return {"cmd": "pc_probe_dump"}, pretty_json + elif cmd == "pc_probe_clear": + return {"cmd": "pc_probe_clear"}, pretty_json elif cmd == "fntrace_arm": # return {"cmd": "fntrace_arm", "target": args[1]}, pretty_json From 02b08dae54d6c720dd8f9d3cb3064bf72f72adad Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 12:28:05 -0400 Subject: [PATCH 2/8] fix CTR graphics --- docs/beetle-linux.md | 19 +++++- runtime/runtime.cmake | 1 + runtime/src/beetle_libretro.cpp | 43 +++++++++++++ runtime/src/debug_server.c | 98 ++++++++++++++++++++++------- runtime/src/dma.c | 106 ++++++++++++++++++++++++-------- runtime/src/gpu.c | 18 ++++++ runtime/src/gte.cpp | 93 ++++++++++++++++++++++++++-- runtime/src/main.cpp | 6 ++ 8 files changed, 330 insertions(+), 54 deletions(-) diff --git a/docs/beetle-linux.md b/docs/beetle-linux.md index a1feb3b0..1d035881 100644 --- a/docs/beetle-linux.md +++ b/docs/beetle-linux.md @@ -19,6 +19,11 @@ git checkout 5759277b # "audit pass" — last C++-tree base we target patch -p1 < ../docs/beetle_wtrace_hook.patch patch -p1 < ../docs/beetle_sio_trace_hook.patch patch -p1 < ../docs/beetle_cdcmd_trace_hook.patch +# cdcmd re-inserts the wtrace globals + typedefs — drop the duplicate block +# in libretro.cpp / mednafen/psx/psx.h if the build errors on redefinition. +# Also needed (not yet in docs/*.patch): rtrace/irq callbacks, guest-cycle +# accumulator, PS_CDC::PSXRecomp_GetDecodeVolume, cdc/dma irq peeks. Apply +# from a prior local beetle-psx tree or re-land those hooks by hand. ``` `beetle_cdcmd_trace_hook.patch` adds a CD-command trace callback (fires per @@ -41,6 +46,16 @@ cd ../runtime cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF -DPSX_DEBUG_TOOLS=ON ninja -C build psx-beetle -# Headless run (picks the next free port from 4380 if taken; watch stderr) -xvfb-run -a ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc +# Run (SDL window; use software renderer if GL context is flaky). +# Beetle looks for lowercase scph5501.bin in the BIOS directory (SHA1 +# 0555C6FA… — SCPH-5501). Symlink or copy next to SCPH1001.BIN. +# Default debug port is 4380 (override with --port). +SDL_RENDER_DRIVER=software ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc --port 4380 + +# Headless (if xvfb is installed): +# xvfb-run -a ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc ``` + +Wire smoke: `{"cmd":"ping"}` → `backend=beetle`. Screenshot via `screenshot`. +`gpu_frame_dump` is a **native** (psx-runtime) command — Beetle does not +expose GP0 OT dumps yet; compare visuals via `screenshot` / `vram_peek`. diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 1d5267e8..e968c9ed 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -192,6 +192,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c ${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c ${PSXRECOMP_ROOT}/runtime/src/dma.c + ${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c ${PSXRECOMP_ROOT}/runtime/src/mdec.c ${PSXRECOMP_ROOT}/runtime/src/timers.c ${PSXRECOMP_ROOT}/runtime/src/interrupts.c diff --git a/runtime/src/beetle_libretro.cpp b/runtime/src/beetle_libretro.cpp index c239e702..d113a721 100644 --- a/runtime/src/beetle_libretro.cpp +++ b/runtime/src/beetle_libretro.cpp @@ -1282,3 +1282,46 @@ extern "C" int beetle_history_get_snapshot(int slot, uint32_t *out_addr, int *ou if (out_active) *out_active = s_beetle_snap_active[slot]; return 1; } + +/* ---- cyc_watch / exc_ring stubs ---- + * Full per-instruction sampling lives in a beetle-psx DebugMode CPUHook that + * was never checked into docs/*.patch. GPU/OT oracle work only needs these + * symbols to link; wire returns empty/unarmed until the hook patch lands. */ +static uint32_t s_cw_anchor_raw = 0, s_cw_anchor_phys = 0; +static uint32_t s_cw_end_raw = 0, s_cw_end_phys = 0; +static uint32_t s_cw_max_hits = 0, s_cw_hits = 0; +static int s_cw_armed = 0; + +extern "C" void beetle_cyc_watch_arm(uint32_t anchor_raw, uint32_t end_raw, int n) { + s_cw_anchor_raw = anchor_raw; + s_cw_anchor_phys = anchor_raw & 0x1FFFFFFFu; + s_cw_end_raw = end_raw; + s_cw_end_phys = end_raw & 0x1FFFFFFFu; + s_cw_max_hits = (n > 0) ? (uint32_t)n : 16u; + s_cw_hits = 0; + s_cw_armed = 1; +} +extern "C" void beetle_cyc_watch_clear(void) { + s_cw_armed = 0; s_cw_hits = 0; +} +extern "C" void beetle_cyc_watch_get_state(uint32_t *anchor_raw, uint32_t *anchor_phys, + uint32_t *end_raw, uint32_t *end_phys, + uint32_t *max_hits, uint32_t *hits, int *armed) { + if (anchor_raw) *anchor_raw = s_cw_anchor_raw; + if (anchor_phys) *anchor_phys = s_cw_anchor_phys; + if (end_raw) *end_raw = s_cw_end_raw; + if (end_phys) *end_phys = s_cw_end_phys; + if (max_hits) *max_hits = s_cw_max_hits; + if (hits) *hits = s_cw_hits; + if (armed) *armed = s_cw_armed; +} +extern "C" int beetle_cyc_watch_get(uint32_t i, uint32_t *hit_index, uint32_t *pc, + unsigned long long *cyc) { + (void)i; (void)hit_index; (void)pc; (void)cyc; + return 0; +} + +extern "C" int retro_psxref_exc_ring_dump(char *out, int cap) { + if (!out || cap < 32) return -1; + return std::snprintf(out, (size_t)cap, "{\"ok\":true,\"entries\":[]}"); +} diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 2d777945..9da263f6 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -16,6 +16,7 @@ #endif #include #include "debug_server.h" +#include "nd_intro_ot.h" #include "latency_ring.h" #include "overlay_loader.h" #include "overlay_capture.h" @@ -2481,10 +2482,13 @@ static inline void pc_probe_observe(uint32_t block_leader_phys) block_leader_phys == 0x0006A600u); int digit_ot = (block_leader_phys == 0x00023094u); int glow_ot = (block_leader_phys == 0x00053E10u); + int digit_rain_ot = (block_leader_phys == 0x0006AE34u); int dispatch = (block_leader_phys == 0x000440A0u); uint32_t ot_ptr = 0; if (digit_ot) ot_ptr = t7; /* caller-supplied OT slot */ + else if (digit_rain_ot) + ot_ptr = cpu->gpr[11]; /* $t3 OT slot from MAC0>>17 helper */ else if (glow_ot) { /* OT head at game+0x147C (same as AddPrim a few insns later). */ uint32_t game = psx_read_word(0x8008D2ACu); @@ -2498,23 +2502,23 @@ static inline void pc_probe_observe(uint32_t block_leader_phys) ot_ptr = fp ? fp : v0; pc_probe_ot_context(cpu, ot_ptr, &mode, &depth, &ot_base, &ot_index); if (wood_ot) { - /* Prefer OTZ as OT index (PsyQ AddPrim(ot+OTZ) style). */ - if (ot_base && otz < (1u << 20)) { - ot_ptr = ot_base + (otz << 2); - ot_index = otz; - } - t0 = a2; + /* WoodEmit jalr $s5 @0x8006A608: MAC0 already swc2'd to 44($at); + * OT base at 56($at) from model+228. Index = MAC0>>17 (== OTZ>>5). */ + uint32_t at = cpu->gpr[1]; + uint32_t mac0 = at ? psx_read_word(at + 44u) : 0; + uint32_t wood_base = at ? psx_read_word(at + 56u) : 0; + uint32_t widx = mac0 >> 17; + if (!widx && otz) + widx = otz >> 5; + ot_base = wood_base; + ot_index = widx; + ot_ptr = wood_base ? (wood_base + (widx << 2)) : 0; + t0 = ot_ptr; /* actual AddPrim OT slot */ depth = otz; - v0 = otz; - /* Pack max screen-X of SXY0/1/2 into mode for extent compares. */ - { - int32_t sx_max = -0x8000; - for (int si = 12; si <= 14; si++) { - int32_t sx = (int32_t)(int16_t)(cpu->gte_data[si] & 0xFFFFu); - if (sx > sx_max) sx_max = sx; - } - mode = (uint32_t)sx_max; - } + v0 = cpu->gpr[21]; /* $s5 helper */ + mode = wood_base; + if (wood_base) + psx_nd_note_wood_batch_ot(wood_base); } else if (digit_ot) { t0 = t7; v0 = ot_index; @@ -2555,14 +2559,17 @@ static inline void pc_probe_observe(uint32_t block_leader_phys) } } else if (block_leader_phys == 0x0006AAF0u) { /* Wood batch setup: $a0 stream, $ra = model (helpers at +92). - * mode=flags@+184, depth=helper@+96, ot_base=name word@+8. */ + * mode=flags@+184, depth=helper@+96, ot_base=name@+8, + * ot_index=batch OT @+228 (live; model RAM is reused later). */ t0 = cpu->gpr[4]; /* a0 */ v0 = cpu->gpr[31]; /* model */ if (v0) { mode = psx_read_word(v0 + 184u); /* draw flags */ depth = psx_read_word(v0 + 96u); /* s5 helper */ ot_base = psx_read_word(v0 + 8u); /* name/tag */ - ot_index = psx_read_word(v0 + 92u); /* pre-emit */ + ot_index = psx_read_word(v0 + 228u); /* batch OT */ + if (ot_index) + psx_nd_note_wood_batch_ot_tagged(ot_index, ot_base); } } else if (block_leader_phys == 0x0006AB58u) { /* Fallthrough past flag bne — batch accepted. */ @@ -2584,13 +2591,32 @@ static inline void pc_probe_observe(uint32_t block_leader_phys) t0 = cpu->gpr[28]; /* gp */ v0 = cpu->gpr[2]; } else if (block_leader_phys == 0x00069BB0u) { - /* Sibling matrix/OT setup entry: a1=ctx (s4+360), a0=mesh. */ + /* Sibling matrix/OT setup entry: a1=ctx (s4+360), a0=mesh. + * ot_index = preferred wood batch OT cache. + * mode = CODE model+228, depth = GLOW model+228 when those + * ND intro models are resident (stable addrs from batch stream). */ t0 = cpu->gpr[5]; /* a1 */ v0 = cpu->gpr[4]; /* a0 */ if (t0) { - mode = psx_read_word(t0 + 244u); /* OT base field before +4092 */ - depth = mode ? (mode + 4092u) : 0; - ot_base = mode; + uint32_t main_ot = psx_read_word(t0 + 244u); + ot_base = main_ot; + /* s4+0xb4 = scene OT bump near WoodEmit batch pools. */ + if (t0 > 0xB4u) + psx_nd_note_sibling_ot_hint(psx_read_word(t0 - 0xB4u)); + } + ot_index = psx_nd_wood_batch_ot(); + { + /* ND intro digit/glow models — confirmed stable during rain. */ + const uint32_t code_m = 0x800FF390u; + const uint32_t glow_m = 0x800FF294u; + uint32_t code_tag = psx_read_word(code_m + 8u); + uint32_t glow_tag = psx_read_word(glow_m + 8u); + mode = (code_tag == 0x45444F43u) ? psx_read_word(code_m + 228u) : 0; + depth = (glow_tag == 0x574F4C47u) ? psx_read_word(glow_m + 228u) : 0; + if (mode) + psx_nd_note_wood_batch_ot_tagged(mode, 0x45444F43u); + else if (depth) + psx_nd_note_wood_batch_ot_tagged(depth, 0x574F4C47u); } } else if (block_leader_phys == 0x00069C34u || block_leader_phys == 0x00069CC4u) { @@ -2600,6 +2626,11 @@ static inline void pc_probe_observe(uint32_t block_leader_phys) mode = cpu->gpr[4]; /* a0 mesh */ depth = t0; ot_base = (t0 >= 4092u) ? (t0 - 4092u) : 0; + } else if (digit_rain_ot) { + /* NdIntroDigitRainCode36: $t3 = OT slot; expose vs game ot_base. */ + t0 = ot_ptr; + v0 = ot_index; + depth = otz; } s->last_t0 = t0; s->last_fp = fp; @@ -2663,6 +2694,29 @@ void debug_server_cyc_observe(uint32_t block_leader_phys) { } } } + /* Cache WoodEmit batch OT at WoodBatchSetup (runs BEFORE sibling on ND + * rain frames) and at scratch load. */ + if (phys == 0x0006AAF0u && debug_cpu_ptr) { + uint32_t model = debug_cpu_ptr->gpr[31]; /* $ra = model */ + if (model) { + uint32_t tag = psx_read_word(model + 8u); + uint32_t base = psx_read_word(model + 228u); + if (base) + psx_nd_note_wood_batch_ot_tagged(base, tag); + } + } + if ((phys == 0x0006ACFCu || phys == 0x0006AD08u) && debug_cpu_ptr) { + uint32_t base = 0; + if (phys == 0x0006ACFCu) { + uint32_t a2 = debug_cpu_ptr->gpr[6]; + if (a2) + base = psx_read_word(a2 + 228u); + } else { + base = debug_cpu_ptr->gpr[5]; /* a1 after lw model+228 */ + } + if (base) + psx_nd_note_wood_batch_ot(base); + } /* ND debug: PSX_ND_SIB_OT_LIFT= adds n*4 to $a3 at sibling face-loop * entry 0x80069CC4 (a3 = *(a1+244)+4092 = last OT slot). Sibling PolyFT3 * right-flap faces use face_hi≈0 → farthest bucket (drawn before additive diff --git a/runtime/src/dma.c b/runtime/src/dma.c index 36bdf547..2bb30e50 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -701,18 +701,45 @@ static uint32_t execute_ch2_gpu(void) { /* Linked-list mode: follow ordering table in RAM. * Each node: bits 24-31 = number of words following header, * bits 0-23 = next node address (0xFFFFFF = end). - * The words following the header are sent to GP0. */ - gpu_ws_begin_linked_list(); - uint32_t addr = - psx_mod_gpu_dma_resolve_address(channels[2].madr); - gpu_ws_prepass_linked_list(addr); - uint32_t safety = 0; - const uint32_t MAX_NODES = 0x40000; /* prevent infinite loops */ + * The words following the header are sent to GP0. + * + * PSX_ND_SIB_FLAP_LAST=1 (opt-in): skip additive PolyGT3 (GP0 0x36) + * whose signed 11-bit SX max is >= 280 AND whose OT rank is in the ND + * digit-rain band (~1832). Was a flap-overpaint workaround; after the + * AVSZ3 MAC0 fix it shreds the crate glow fountain — leave unset/0. + * + * OT-rank gate is mandatory: the same SX filter matches menu/char-select + * 0x36 (e.g. ot~1066) and would strip Crash face/trophy semis. Rank is + * the empty-node count used by gpu_set_gp0_linked_list_node. + * + * SX must be parsed as signed 11-bit (gpu.c parse_vertex). Legacy alias: + * PSX_ND_OT_OPAQUE_LAST. */ + static int s_nd_flap_last = -1; + if (s_nd_flap_last < 0) { + const char *e = getenv("PSX_ND_SIB_FLAP_LAST"); + if (!e || !*e) + e = getenv("PSX_ND_OT_OPAQUE_LAST"); + s_nd_flap_last = (e && *e && *e != '0') ? 1 : 0; + if (s_nd_flap_last) + fprintf(stdout, "psxrecomp: PSX_ND_SIB_FLAP_LAST enabled\n"); + } + gpu_ws_begin_linked_list(); + uint32_t start_addr = + psx_mod_gpu_dma_resolve_address(channels[2].madr); + gpu_ws_prepass_linked_list(start_addr); + const uint32_t MAX_NODES = 0x40000; /* prevent infinite loops */ + uint32_t last_addr = start_addr; + int hit_limit = 0; + /* Mirror gpu.c empty-node OT rank (0 after first empty header). */ + uint32_t ot_rank = 0xffffffffu; + + uint32_t addr = start_addr; + uint32_t safety = 0; for (;;) { if (safety++ > MAX_NODES) { - /* Abort this transfer — linked list has a cycle or is corrupt. - * Don't crash; the shell may recover on the next frame. */ + hit_limit = 1; + last_addr = addr; break; } @@ -720,29 +747,60 @@ static uint32_t execute_ch2_gpu(void) { uint32_t num_words = (header >> 24) & 0xFF; uint32_t word_addr = psx_mod_gpu_dma_resolve_address(addr + 4u); - gpu_set_gp0_linked_list_node(addr, num_words); - actual_words += 1u; - for (uint32_t i = 0; i < num_words; i++) { - uint32_t word = psx_read_word(word_addr); - gpu_set_gp0_source(word_addr); - gpu_write_gp0(word); - word_addr = - psx_mod_gpu_dma_resolve_address(word_addr + 4u); + if (num_words == 0u) + ot_rank = (ot_rank == 0xffffffffu) ? 0u : (ot_rank + 1u); + + int emit = 1; + if (s_nd_flap_last && num_words >= 6u && + ot_rank >= 1600u && ot_rank < 2100u) { + uint32_t cmd = psx_read_word(word_addr); + uint32_t op = (cmd >> 24) & 0xFFu; + if (op == 0x36u) { + /* Match gpu.c parse_vertex: signed 11-bit SX. Digit-rain + * packets often stash junk in the high bits of the XY word; + * int16 SX mis-classifies them. Skip glow whose s11 bbox + * reaches the sibling right-flap band (sx_max >= 280). */ + int32_t sx_max = -0x8000; + for (uint32_t vi = 1; vi <= 5; vi += 2) { + uint32_t xy = psx_read_word( + psx_mod_gpu_dma_resolve_address(word_addr + + vi * 4u)); + int32_t sx = (int32_t)(xy & 0x7FFu); + if (sx & 0x400) + sx -= 0x800; + if (sx > sx_max) + sx_max = sx; + } + if (sx_max >= 280) + emit = 0; + } + } + + if (emit) { + gpu_set_gp0_linked_list_node(addr, num_words); + actual_words += 1u; + for (uint32_t i = 0; i < num_words; i++) { + uint32_t word = psx_read_word(word_addr); + gpu_set_gp0_source(word_addr); + gpu_write_gp0(word); + word_addr = + psx_mod_gpu_dma_resolve_address(word_addr + 4u); + } + actual_words += num_words; } - actual_words += num_words; - /* Next node */ uint32_t next = header & 0xFFFFFFu; if (next == 0xFFFFFFu) { - channels[2].madr = 0x00FFFFFFu; - break; /* end of list */ + last_addr = 0x00FFFFFFu; + break; } addr = psx_mod_gpu_dma_resolve_address(next); + last_addr = addr; } - if (safety > MAX_NODES) { - channels[2].madr = addr; - } + (void)hit_limit; + + channels[2].madr = last_addr; gpu_ws_end_linked_list(); } else { /* Burst mode (sync_mode == 0) */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 6706bc87..d13c8a75 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3236,6 +3236,24 @@ static void gp0_exec_mono_quad(void) { } if (draw_area_out_bbox(vx, vy, 4)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); + /* Semi axis-aligned mono quads (UI boxes/borders): one rect, not two tris. + * Thin semi borders (e.g. CTR name-entry OT-1144 teal 3×H) otherwise double- + * blend their shared diagonal — nearly the whole strip — and overpaint 3D. */ + if (semi_trans && ws_axis_aligned_quad(vx, vy)) { + int32_t min_x = vx[0], max_x = vx[0], min_y = vy[0], max_y = vy[0]; + for (int i = 1; i < 4; i++) { + if (vx[i] < min_x) min_x = vx[i]; + if (vx[i] > max_x) max_x = vx[i]; + if (vy[i] < min_y) min_y = vy[i]; + if (vy[i] > max_y) max_y = vy[i]; + } + int w = (int)(max_x - min_x); + int h = (int)(max_y - min_y); + if (w > 0 && h > 0) { + gr_draw_flat_rect(min_x, min_y, w, h, color); + return; + } + } if (!rej_a) { int32_t tx[3] = { vx[0], vx[1], vx[2] }; int32_t ty[3] = { vy[0], vy[1], vy[2] }; diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index c366b7a1..7c8ffd94 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1,10 +1,13 @@ #include "gte.h" #include "cpu_state.h" +#include "nd_intro_ot.h" #include #include #include #include +extern "C" uint32_t psx_read_word(uint32_t addr); + namespace PSXRecomp { namespace GTE { @@ -996,12 +999,19 @@ void gte_nclip(GTEState* gte, uint32_t instr) { // --------------------------------------------------------------------------- // AVSZ3 (0x2D) — Average Z (3 points) // --------------------------------------------------------------------------- +// Hardware (psx-spx / DuckStation): MAC0 keeps ZSF3*(SZ1+SZ2+SZ3); only OTZ +// gets MAC0/1000h. CTR NdIntroWoodEmitHelper indexes the batch OT with +// MAC0>>17 (== OTZ>>5). Shifting MAC0 here collapsed every face into slot 0 +// (arms/trophy/chest z-fight + missing banner under later same-bucket paint). void gte_avsz3(GTEState* gte, uint32_t instr) { + (void)instr; gte->FLAG = 0; - int64_t mac0 = (int64_t)gte->ZSF3 * (gte->SZ[1] + gte->SZ[2] + gte->SZ[3]); + const int64_t sum = + (int64_t)(uint32_t)gte->SZ[1] + gte->SZ[2] + gte->SZ[3]; + const int64_t mac0 = (int64_t)gte->ZSF3 * sum; gte->check_mac0_overflow(mac0); - gte->MAC0 = static_cast(mac0 >> 12); - gte->OTZ = gte->saturate_sz(gte->MAC0); + gte->MAC0 = static_cast(mac0); + gte->OTZ = gte->saturate_sz(static_cast(mac0 >> 12)); gte->set_error_flag(); } @@ -1009,11 +1019,14 @@ void gte_avsz3(GTEState* gte, uint32_t instr) { // AVSZ4 (0x2E) — Average Z (4 points) // --------------------------------------------------------------------------- void gte_avsz4(GTEState* gte, uint32_t instr) { + (void)instr; gte->FLAG = 0; - int64_t mac0 = (int64_t)gte->ZSF4 * (gte->SZ[0] + gte->SZ[1] + gte->SZ[2] + gte->SZ[3]); + const int64_t sum = (int64_t)(uint32_t)gte->SZ[0] + gte->SZ[1] + + gte->SZ[2] + gte->SZ[3]; + const int64_t mac0 = (int64_t)gte->ZSF4 * sum; gte->check_mac0_overflow(mac0); - gte->MAC0 = static_cast(mac0 >> 12); - gte->OTZ = gte->saturate_sz(gte->MAC0); + gte->MAC0 = static_cast(mac0); + gte->OTZ = gte->saturate_sz(static_cast(mac0 >> 12)); gte->set_error_flag(); } @@ -1811,6 +1824,74 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) { gte_export_cpu_state(cpu, >e); +#ifndef PSX_NO_DEBUG_TOOLS + /* ND digit-rain: NdIntroSiblingFaceLoop (jal 0x80069CC4; $ra stays 69C3C.. + * while FaceLoop runs). face_hi=0 → AddPrim at a3 (main OT last slot). + * WoodEmit/glow: model+228 base + (MAC0>>17)*4 via 0x8006AD88. + * + * Frame order on rain: WoodBatchSetup (all models, incl. CODE/GLOW) → + * sibling → WoodEmit. CODE model is stable at 0x800FF390. + * + * PSX_ND_SIB_OT_BATCH=1: $s2 = CODE/GLOW batch OT + index*4. + * PSX_ND_SIB_OT_IDX=N: force index (else OTZ>>5 + PSX_ND_SIB_OT_BIAS). + * Same-bucket as digits still loses: DMA walks flaps then 0x36 (digits on + * top). Prefer correct AVSZ MAC0 OT indices; FLAP_LAST is opt-in only. */ + if (func == 0x30 && !s_gte_replay_sandbox) { + static int s_sib_batch = -1; + static int s_sib_avsz = -1; + static int s_sib_bias = 0; + static int s_sib_force_idx = -1; + if (s_sib_batch < 0) { + const char *e = std::getenv("PSX_ND_SIB_OT_BATCH"); + s_sib_batch = (e && *e && *e != '0') ? 1 : 0; + const char *a = std::getenv("PSX_ND_SIB_OT_AVSZ"); + s_sib_avsz = (a && *a && *a != '0') ? 1 : 0; + const char *b = std::getenv("PSX_ND_SIB_OT_BIAS"); + s_sib_bias = (b && *b) ? std::atoi(b) : 0; + const char *ix = std::getenv("PSX_ND_SIB_OT_IDX"); + s_sib_force_idx = (ix && *ix) ? std::atoi(ix) : -1; + if (s_sib_batch) + std::fprintf(stdout, + "psxrecomp: PSX_ND_SIB_OT_BATCH enabled (bias=%d idx=%d)\n", + s_sib_bias, s_sib_force_idx); + else if (s_sib_avsz) + std::fprintf(stdout, + "psxrecomp: PSX_ND_SIB_OT_AVSZ enabled (main OTZ>>5, bias=%d)\n", + s_sib_bias); + } + if (s_sib_batch || s_sib_avsz) { + const uint32_t ra = s_gte_caller_ra; + if (ra == 0x80069C3Cu || ra == 0x80069C60u || + ra == 0x80069C84u || ra == 0x80069CA8u) { + const uint32_t a3 = cpu->gpr[7]; + uint32_t ot_base = 0; + if (s_sib_batch) { + /* CODE model+228 preferred (digit rain); noted at 6AAF0 + * and refreshed at sibling entry from 0x800FF390. */ + ot_base = psx_nd_wood_batch_ot(); + if (!ot_base) + ot_base = psx_nd_sibling_ot_hint(); + } else if (a3 >= 4092u) { + ot_base = a3 - 4092u; + } + if (ot_base) { + int32_t idx; + if (s_sib_force_idx >= 0) { + idx = s_sib_force_idx; + } else { + const int64_t mac0 = (int64_t)gte.ZSF3 * + ((int64_t)gte.SZ[1] + gte.SZ[2] + gte.SZ[3]); + idx = (int32_t)(mac0 >> 17) + s_sib_bias; + } + if (idx < 0) idx = 0; + if (idx > 2047) idx = 2047; + cpu->gpr[18] = ot_base + ((uint32_t)idx << 2); /* $s2 */ + } + } + } + } +#endif + #ifdef PSX_ENABLE_BLOCK_CYCLES /* Faithful GTE command completion-stall: arm the per-command deadline * (serializing back-to-back ops). Any later COP2 register access stalls to diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f33beeb3..c4e70b24 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -8883,6 +8883,12 @@ int main(int argc, char** argv) { game_id = gc.id; game_region = gc.region; game_players = gc.players; + /* CTR ND intro: an older workaround (PSX_ND_SIB_FLAP_LAST=1) skipped + * wide additive 0x36 in OT ranks 1600..2099 to unmask sibling flaps. + * After the AVSZ3 MAC0 fix (unshifted product → correct MAC0>>17 OT + * indices), flaps/rain sort without that skip — and FLAP_LAST=1 + * shreds the crate glow fountain. Default is off; opt in via env + * (dma.c) only for experiments. */ apply_offline_pad_count(game_players, multitap_enabled); game_has_disc_crc = gc.has_disc_crc; game_disc_crc = gc.disc_crc; From 083959df5197a0d478d4dd8b71f5a0ad769db8c8 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 12:29:59 -0400 Subject: [PATCH 3/8] intro diagnostic and runtime patches --- runtime/include/nd_intro_ot.h | 24 +++++++++++++++ runtime/src/nd_intro_ot.c | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 runtime/include/nd_intro_ot.h create mode 100644 runtime/src/nd_intro_ot.c diff --git a/runtime/include/nd_intro_ot.h b/runtime/include/nd_intro_ot.h new file mode 100644 index 00000000..71a50553 --- /dev/null +++ b/runtime/include/nd_intro_ot.h @@ -0,0 +1,24 @@ +/* nd_intro_ot.h — CTR Naughty Dog intro OT helpers (debug / accuracy shims). */ +#ifndef PSXRECOMP_ND_INTRO_OT_H +#define PSXRECOMP_ND_INTRO_OT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Last WoodEmit batch OT base (model+228). Prefer digit-rain tags. */ +void psx_nd_note_wood_batch_ot(uint32_t base); +void psx_nd_note_wood_batch_ot_tagged(uint32_t base, uint32_t tag); +uint32_t psx_nd_wood_batch_ot(void); + +/* Sibling-scene bump hint at s4+0xb4 (captured at NdIntroSiblingRtptEmit). */ +void psx_nd_note_sibling_ot_hint(uint32_t base); +uint32_t psx_nd_sibling_ot_hint(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/src/nd_intro_ot.c b/runtime/src/nd_intro_ot.c new file mode 100644 index 00000000..5f9555a9 --- /dev/null +++ b/runtime/src/nd_intro_ot.c @@ -0,0 +1,56 @@ +/* nd_intro_ot.c — CTR ND intro OT batch base cache for sibling flap depth. */ +#include "nd_intro_ot.h" + +static uint32_t s_wood_batch_ot; +static uint32_t s_wood_batch_ot_code; +static uint32_t s_wood_batch_ot_glow; +static uint32_t s_wood_batch_ot_box; +static uint32_t s_sibling_ot_hint; + +static int looks_like_ot_base(uint32_t base) +{ + /* Wood batch OTs sit in gameplay RAM below the main OT windows. */ + return base >= 0x800F0000u && base < 0x800F8000u && (base & 3u) == 0u; +} + +void psx_nd_note_wood_batch_ot(uint32_t base) +{ + if (looks_like_ot_base(base)) + s_wood_batch_ot = base; +} + +void psx_nd_note_wood_batch_ot_tagged(uint32_t base, uint32_t tag) +{ + if (!looks_like_ot_base(base)) + return; + s_wood_batch_ot = base; + /* Prefer CODE (digit rain) > GLOW > BOX_ for sibling OT rewrite. */ + if (tag == 0x45444F43u) /* CODE */ + s_wood_batch_ot_code = base; + else if (tag == 0x574F4C47u) /* GLOW */ + s_wood_batch_ot_glow = base; + else if (tag == 0x5F584F42u) /* BOX_ */ + s_wood_batch_ot_box = base; +} + +uint32_t psx_nd_wood_batch_ot(void) +{ + if (s_wood_batch_ot_code) + return s_wood_batch_ot_code; + if (s_wood_batch_ot_glow) + return s_wood_batch_ot_glow; + if (s_wood_batch_ot_box) + return s_wood_batch_ot_box; + return s_wood_batch_ot; +} + +void psx_nd_note_sibling_ot_hint(uint32_t base) +{ + if (looks_like_ot_base(base)) + s_sibling_ot_hint = base; +} + +uint32_t psx_nd_sibling_ot_hint(void) +{ + return s_sibling_ot_hint; +} From 6745d649aafe1b2859654f5a19861a940bf22c49 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 13:17:48 -0400 Subject: [PATCH 4/8] Update recomp-net --- lib/recomp-net | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recomp-net b/lib/recomp-net index 198ebc81..0985f537 160000 --- a/lib/recomp-net +++ b/lib/recomp-net @@ -1 +1 @@ -Subproject commit 198ebc8160661f2a39139d24c3b6cb5eefcfbdba +Subproject commit 0985f537728f95cc0f96d748a6b0242a04b58faf From d1dc8143c5d62612e730941e8c34a01c21936581 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 14:14:19 -0400 Subject: [PATCH 5/8] Update recomp-net --- lib/recomp-net | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/recomp-net b/lib/recomp-net index 0985f537..6d848d6f 160000 --- a/lib/recomp-net +++ b/lib/recomp-net @@ -1 +1 @@ -Subproject commit 0985f537728f95cc0f96d748a6b0242a04b58faf +Subproject commit 6d848d6f9a7269d511e6e97d943c2ef0774779b0 From c53025ed1397ec670a685f50f96aa99c54a2a9e9 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 20:03:06 -0400 Subject: [PATCH 6/8] bump toolchain --- runtime/runtime.cmake | 11 +++++++++++ tools/toolchain_pack.py | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index f4f7a359..250e5466 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -97,7 +97,18 @@ if(_psx_sdl_backend STREQUAL "SDL3") option(PSX_SDL3_FETCH "Fetch the pinned SDL3 release when no system SDL3 package is found" ON) + # cmake-clang-v1 1.0.7+ ships static SDL3 under the pack root. Prefer that + # over FetchContent (avoids hundreds of Windows try_compile probes). + foreach(_psx_tc_env IN ITEMS RETCOMM_TOOLCHAIN_DIR PSXRECOMP_TOOLCHAIN_DIR) + if(DEFINED ENV{${_psx_tc_env}} AND NOT "$ENV{${_psx_tc_env}}" STREQUAL "") + list(PREPEND CMAKE_PREFIX_PATH "$ENV{${_psx_tc_env}}") + endif() + endforeach() + list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH) find_package(SDL3 3.4 CONFIG QUIET COMPONENTS SDL3) + if(TARGET SDL3::SDL3) + message(STATUS "psxrecomp: using prebuilt/system SDL3 (skip FetchContent)") + endif() if(NOT TARGET SDL3::SDL3 AND PSX_SDL3_FETCH) include(FetchContent) # The fetched dependency is private to this build, so link it directly diff --git a/tools/toolchain_pack.py b/tools/toolchain_pack.py index d2d6ad24..168c3e75 100644 --- a/tools/toolchain_pack.py +++ b/tools/toolchain_pack.py @@ -481,7 +481,7 @@ def activate_toolchain_bin(bin_dir: Path, log=None) -> None: # Idempotent session PATH: only prepend when missing. if str(bin_dir) not in parts and prefix not in parts: os.environ["PATH"] = prefix + (os.pathsep + cur if cur else "") - # Windows cmake-clang-v1 ships zlib under the pack root; help FindZLIB. + # Pack root hosts zlib (Windows) + static SDL3 (1.0.7+) for find_package. pack_s = str(pack_root) os.environ["ZLIB_ROOT"] = pack_s os.environ["RETCOMM_TOOLCHAIN_DIR"] = pack_s @@ -492,6 +492,11 @@ def activate_toolchain_bin(bin_dir: Path, log=None) -> None: os.environ["CMAKE_PREFIX_PATH"] = pack_s elif pack_s not in prev_prefix.split(os.pathsep): os.environ["CMAKE_PREFIX_PATH"] = pack_s + os.pathsep + prev_prefix + # Hint CONFIG packages when present (harmless if absent on older packs). + sdl3_config = pack_root / "lib" / "cmake" / "SDL3" / "SDL3Config.cmake" + sdl3_config_alt = pack_root / "lib" / "cmake" / "SDL3" / "SDL3-config.cmake" + if sdl3_config.is_file() or sdl3_config_alt.is_file(): + os.environ["SDL3_DIR"] = str(pack_root / "lib" / "cmake" / "SDL3") lib_dir = pack_root / "lib" if lib_dir.is_dir() and not sys_platform_is_windows(): lib_s = str(lib_dir) From d3f979154abd2653f578fa63187ff2505449b789 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 20:25:32 -0400 Subject: [PATCH 7/8] bumpt --- docs/ci/templates/setup-release.yml | 7 ++++ runtime/runtime.cmake | 52 +++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/docs/ci/templates/setup-release.yml b/docs/ci/templates/setup-release.yml index 363f5982..248e434b 100644 --- a/docs/ci/templates/setup-release.yml +++ b/docs/ci/templates/setup-release.yml @@ -263,6 +263,13 @@ jobs: EXTRA+=("-DGLSLC_EXE=$(command -v glslc)") fi JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" + # Emitters may have set PSXRECOMP_TOOLCHAIN_DIR (llvm-mingw) via + # GITHUB_ENV. Clear it before the MSYS2 g++ setup-host configure so + # pack lib/ cannot win -L for -static-libstdc++ (codecvt link errors). + if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then + unset PSXRECOMP_TOOLCHAIN_DIR TOOLCHAIN_DIR BPE_TOOLCHAIN_DIR RETCOMM_TOOLCHAIN_DIR || true + unset CMAKE_PREFIX_PATH SDL3_DIR ZLIB_ROOT || true + fi # Setup host: no game C, no BIOS backends linked. clear_generated.sh # already wiped generated/; end users Generate OpenBIOS (+ optional # SCPH1001) locally via the wizard, then rebuild. diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 250e5466..d262ab75 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -92,20 +92,41 @@ set(PSX_SDL_LIBRARIES "") set(PSX_SDL_STATIC_LDFLAGS "") set(PSX_SDL3 OFF) +# Portable cmake-clang-v1 pack roots (wizard / RetComM / CI emitter fetch). +# Collect as HINTS only — never list(PREPEND CMAKE_PREFIX_PATH …): on Windows CI +# the pack is llvm-mingw while the setup host links with MSYS2 g++, and a +# global prefix puts pack lib/ on the -L path so -static-libstdc++ can pick up +# the wrong libstdc++ (codecvt / filesystem undefined refs at link). +set(_PSX_TOOLCHAIN_PREFIX_HINTS "") +foreach(_psx_tc_env IN ITEMS RETCOMM_TOOLCHAIN_DIR PSXRECOMP_TOOLCHAIN_DIR + BPE_TOOLCHAIN_DIR TOOLCHAIN_DIR) + if(DEFINED ENV{${_psx_tc_env}} AND NOT "$ENV{${_psx_tc_env}}" STREQUAL "") + list(APPEND _PSX_TOOLCHAIN_PREFIX_HINTS "$ENV{${_psx_tc_env}}") + endif() +endforeach() +list(REMOVE_DUPLICATES _PSX_TOOLCHAIN_PREFIX_HINTS) + if(_psx_sdl_backend STREQUAL "SDL3") set(PSX_SDL3 ON) option(PSX_SDL3_FETCH "Fetch the pinned SDL3 release when no system SDL3 package is found" ON) - # cmake-clang-v1 1.0.7+ ships static SDL3 under the pack root. Prefer that - # over FetchContent (avoids hundreds of Windows try_compile probes). - foreach(_psx_tc_env IN ITEMS RETCOMM_TOOLCHAIN_DIR PSXRECOMP_TOOLCHAIN_DIR) - if(DEFINED ENV{${_psx_tc_env}} AND NOT "$ENV{${_psx_tc_env}}" STREQUAL "") - list(PREPEND CMAKE_PREFIX_PATH "$ENV{${_psx_tc_env}}") - endif() - endforeach() - list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH) - find_package(SDL3 3.4 CONFIG QUIET COMPONENTS SDL3) + # cmake-clang-v1 1.0.7+ ships static SDL3 (lib/cmake/SDL3). Prefer via + # SDL3_DIR / HINTS without mutating CMAKE_PREFIX_PATH. + if(NOT SDL3_DIR) + foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS) + if(EXISTS "${_psx_tc_pfx}/lib/cmake/SDL3/SDL3Config.cmake") + set(SDL3_DIR "${_psx_tc_pfx}/lib/cmake/SDL3") + break() + elseif(EXISTS "${_psx_tc_pfx}/lib/cmake/SDL3/SDL3-config.cmake") + set(SDL3_DIR "${_psx_tc_pfx}/lib/cmake/SDL3") + break() + endif() + endforeach() + endif() + find_package(SDL3 3.4 CONFIG QUIET COMPONENTS SDL3 + HINTS ${_PSX_TOOLCHAIN_PREFIX_HINTS} + PATH_SUFFIXES lib/cmake/SDL3) if(TARGET SDL3::SDL3) message(STATUS "psxrecomp: using prebuilt/system SDL3 (skip FetchContent)") endif() @@ -538,6 +559,17 @@ function(psxrecomp_ensure_zlib) if(TARGET ZLIB::ZLIB) return() endif() + # Prefer pack zlib via ZLIB_ROOT (full path to libz.a) — not CMAKE_PREFIX_PATH. + if(NOT ZLIB_ROOT) + foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS) + if(EXISTS "${_psx_tc_pfx}/include/zlib.h" AND + (EXISTS "${_psx_tc_pfx}/lib/libz.a" OR + EXISTS "${_psx_tc_pfx}/lib/zlib.lib")) + set(ZLIB_ROOT "${_psx_tc_pfx}") + break() + endif() + endforeach() + endif() find_package(ZLIB QUIET) if(TARGET ZLIB::ZLIB) return() @@ -549,7 +581,7 @@ function(psxrecomp_ensure_zlib) "zlib-devel / mingw-w64-zlib, or configure with -DPSX_ZLIB_FETCH=ON.") endif() message(STATUS - "psxrecomp: ZLIB not in CMAKE_PREFIX_PATH/ZLIB_ROOT; " + "psxrecomp: ZLIB not in ZLIB_ROOT / system paths; " "fetching zlib 1.3.1 (prefer a toolchain pack that bundles it)") include(FetchContent) set(_psx_zlib_timestamp_args "") From e8fbceeca08f40478449542202e4647438b8f62f Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 21:11:34 -0400 Subject: [PATCH 8/8] bump windows build errors --- runtime/runtime.cmake | 46 +++++++++++++++++++++++++++-------------- tools/toolchain_pack.py | 36 ++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index d262ab75..7904cf08 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -111,22 +111,30 @@ if(_psx_sdl_backend STREQUAL "SDL3") option(PSX_SDL3_FETCH "Fetch the pinned SDL3 release when no system SDL3 package is found" ON) - # cmake-clang-v1 1.0.7+ ships static SDL3 (lib/cmake/SDL3). Prefer via - # SDL3_DIR / HINTS without mutating CMAKE_PREFIX_PATH. + # cmake-clang-v1 1.0.9+ ships SDL3 under deps/; 1.0.7–1.0.8 used pack root. + # Prefer SDL3_DIR / HINTS — never CMAKE_PREFIX_PATH=pack (mingw include poisons libc++). if(NOT SDL3_DIR) foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS) - if(EXISTS "${_psx_tc_pfx}/lib/cmake/SDL3/SDL3Config.cmake") - set(SDL3_DIR "${_psx_tc_pfx}/lib/cmake/SDL3") - break() - elseif(EXISTS "${_psx_tc_pfx}/lib/cmake/SDL3/SDL3-config.cmake") - set(SDL3_DIR "${_psx_tc_pfx}/lib/cmake/SDL3") + foreach(_psx_sdl_root IN ITEMS "${_psx_tc_pfx}/deps" "${_psx_tc_pfx}") + if(EXISTS "${_psx_sdl_root}/lib/cmake/SDL3/SDL3Config.cmake" OR + EXISTS "${_psx_sdl_root}/lib/cmake/SDL3/SDL3-config.cmake") + set(SDL3_DIR "${_psx_sdl_root}/lib/cmake/SDL3") + break() + endif() + endforeach() + if(SDL3_DIR) break() endif() endforeach() endif() + set(_PSX_SDL3_HINTS "") + foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS) + list(APPEND _PSX_SDL3_HINTS "${_psx_tc_pfx}/deps" "${_psx_tc_pfx}") + endforeach() find_package(SDL3 3.4 CONFIG QUIET COMPONENTS SDL3 - HINTS ${_PSX_TOOLCHAIN_PREFIX_HINTS} + HINTS ${_PSX_SDL3_HINTS} PATH_SUFFIXES lib/cmake/SDL3) + unset(_PSX_SDL3_HINTS) if(TARGET SDL3::SDL3) message(STATUS "psxrecomp: using prebuilt/system SDL3 (skip FetchContent)") endif() @@ -224,7 +232,6 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c ${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c ${PSXRECOMP_ROOT}/runtime/src/dma.c - ${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c ${PSXRECOMP_ROOT}/runtime/src/mdec.c ${PSXRECOMP_ROOT}/runtime/src/timers.c ${PSXRECOMP_ROOT}/runtime/src/interrupts.c @@ -559,13 +566,19 @@ function(psxrecomp_ensure_zlib) if(TARGET ZLIB::ZLIB) return() endif() - # Prefer pack zlib via ZLIB_ROOT (full path to libz.a) — not CMAKE_PREFIX_PATH. + # Prefer pack zlib via ZLIB_ROOT — deps/ (1.0.9+) then legacy pack root. + # Never use CMAKE_PREFIX_PATH=pack on Windows llvm-mingw (libc++ clash). if(NOT ZLIB_ROOT) foreach(_psx_tc_pfx IN LISTS _PSX_TOOLCHAIN_PREFIX_HINTS) - if(EXISTS "${_psx_tc_pfx}/include/zlib.h" AND - (EXISTS "${_psx_tc_pfx}/lib/libz.a" OR - EXISTS "${_psx_tc_pfx}/lib/zlib.lib")) - set(ZLIB_ROOT "${_psx_tc_pfx}") + foreach(_psx_z_root IN ITEMS "${_psx_tc_pfx}/deps" "${_psx_tc_pfx}") + if(EXISTS "${_psx_z_root}/include/zlib.h" AND + (EXISTS "${_psx_z_root}/lib/libz.a" OR + EXISTS "${_psx_z_root}/lib/zlib.lib")) + set(ZLIB_ROOT "${_psx_z_root}") + break() + endif() + endforeach() + if(ZLIB_ROOT) break() endif() endforeach() @@ -577,8 +590,9 @@ function(psxrecomp_ensure_zlib) if(NOT PSX_ZLIB_FETCH) message(FATAL_ERROR "ZLIB was not found. On Windows, use a cmake-clang-v1 pack that " - "ships include/zlib.h + lib/libz.a (sets ZLIB_ROOT), or install " - "zlib-devel / mingw-w64-zlib, or configure with -DPSX_ZLIB_FETCH=ON.") + "ships deps/include/zlib.h + deps/lib/libz.a (sets ZLIB_ROOT), or " + "install zlib-devel / mingw-w64-zlib, or configure with " + "-DPSX_ZLIB_FETCH=ON.") endif() message(STATUS "psxrecomp: ZLIB not in ZLIB_ROOT / system paths; " diff --git a/tools/toolchain_pack.py b/tools/toolchain_pack.py index 168c3e75..dde92cb0 100644 --- a/tools/toolchain_pack.py +++ b/tools/toolchain_pack.py @@ -481,22 +481,36 @@ def activate_toolchain_bin(bin_dir: Path, log=None) -> None: # Idempotent session PATH: only prepend when missing. if str(bin_dir) not in parts and prefix not in parts: os.environ["PATH"] = prefix + (os.pathsep + cur if cur else "") - # Pack root hosts zlib (Windows) + static SDL3 (1.0.7+) for find_package. + # Host deps under deps/ (1.0.9+); legacy packs keep zlib/SDL3 at pack root. + # Never put pack root on CMAKE_PREFIX_PATH — mingw include/ poisons libc++. pack_s = str(pack_root) - os.environ["ZLIB_ROOT"] = pack_s + deps_root = pack_root / "deps" os.environ["RETCOMM_TOOLCHAIN_DIR"] = pack_s if "PSXRECOMP_TOOLCHAIN_DIR" not in os.environ: os.environ["PSXRECOMP_TOOLCHAIN_DIR"] = pack_s + if (deps_root / "include" / "zlib.h").is_file(): + os.environ["ZLIB_ROOT"] = str(deps_root) + elif (pack_root / "include" / "zlib.h").is_file(): + os.environ["ZLIB_ROOT"] = pack_s prev_prefix = os.environ.get("CMAKE_PREFIX_PATH", "") - if not prev_prefix: - os.environ["CMAKE_PREFIX_PATH"] = pack_s - elif pack_s not in prev_prefix.split(os.pathsep): - os.environ["CMAKE_PREFIX_PATH"] = pack_s + os.pathsep + prev_prefix - # Hint CONFIG packages when present (harmless if absent on older packs). - sdl3_config = pack_root / "lib" / "cmake" / "SDL3" / "SDL3Config.cmake" - sdl3_config_alt = pack_root / "lib" / "cmake" / "SDL3" / "SDL3-config.cmake" - if sdl3_config.is_file() or sdl3_config_alt.is_file(): - os.environ["SDL3_DIR"] = str(pack_root / "lib" / "cmake" / "SDL3") + if prev_prefix: + filtered = [ + p for p in prev_prefix.split(os.pathsep) + if p and os.path.normcase(os.path.normpath(p)) + != os.path.normcase(os.path.normpath(pack_s)) + ] + os.environ["CMAKE_PREFIX_PATH"] = os.pathsep.join(filtered) + elif "CMAKE_PREFIX_PATH" in os.environ: + del os.environ["CMAKE_PREFIX_PATH"] + sdl3_dir = None + for root in (deps_root, pack_root): + cfg = root / "lib" / "cmake" / "SDL3" / "SDL3Config.cmake" + cfg_alt = root / "lib" / "cmake" / "SDL3" / "SDL3-config.cmake" + if cfg.is_file() or cfg_alt.is_file(): + sdl3_dir = root / "lib" / "cmake" / "SDL3" + break + if sdl3_dir is not None: + os.environ["SDL3_DIR"] = str(sdl3_dir) lib_dir = pack_root / "lib" if lib_dir.is_dir() and not sys_platform_is_windows(): lib_s = str(lib_dir)