fix CTR graphics

This commit is contained in:
Alex Vanderveen
2026-08-08 12:28:05 -04:00
parent ea8f1308e6
commit 02b08dae54
8 changed files with 330 additions and 54 deletions
+17 -2
View File
@@ -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 <game.cue>
# 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 <game.cue> --port 4380
# Headless (if xvfb is installed):
# xvfb-run -a ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc <game.cue>
```
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`.
+1
View File
@@ -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
+43
View File
@@ -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\":[]}");
}
+76 -22
View File
@@ -16,6 +16,7 @@
#endif
#include <time.h>
#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=<n> 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
+82 -24
View File
@@ -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) */
+18
View File
@@ -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] };
+87 -6
View File
@@ -1,10 +1,13 @@
#include "gte.h"
#include "cpu_state.h"
#include "nd_intro_ot.h"
#include <algorithm>
#include <cstdlib>
#include <cstdio>
#include <cstring>
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<int32_t>(mac0 >> 12);
gte->OTZ = gte->saturate_sz(gte->MAC0);
gte->MAC0 = static_cast<int32_t>(mac0);
gte->OTZ = gte->saturate_sz(static_cast<int32_t>(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<int32_t>(mac0 >> 12);
gte->OTZ = gte->saturate_sz(gte->MAC0);
gte->MAC0 = static_cast<int32_t>(mac0);
gte->OTZ = gte->saturate_sz(static_cast<int32_t>(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, &gte);
#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
+6
View File
@@ -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;