From 02b08dae54d6c720dd8f9d3cb3064bf72f72adad Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sat, 8 Aug 2026 12:28:05 -0400 Subject: [PATCH] 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;