Strip session instrumentation for release; gate offset census behind ARMSX_TEST_OFFSET_CENSUS

This commit is contained in:
jpolo1224
2026-08-02 12:34:01 -04:00
parent 0d22376375
commit 29aac6c04a
7 changed files with 45 additions and 1107 deletions
+7 -1
View File
@@ -536,9 +536,15 @@ TEST_GPU_SOURCES := tests/gpu_renderer_parity.c psx/dev/gpu.c psx/perf.c psx/pgx
# (PSX_GPU_MASK_WRITE / PSX_GPU_MASK_SKIP) live there and the test asserts them directly, so a
# header-only change has to relink or the gate keeps passing against a stale binary — which it
# did, silently, when the contract test was first written.
# -DARMSX_TEST_OFFSET_CENSUS is deliberately set HERE AND NOWHERE ELSE. It compiles in the
# drawing-offset census that `offset-stream-integrity` asserts on (psx/dev/gpu.h). That census
# runs once per primitive inside gpu_render_triangle() / _rect() / _flat_line(), so leaving it
# in a shipped binary would put ~1800 calls a frame in the hottest function in the emulator and
# hand a PGO run a profile shaped by test-only code. Adding this flag to any other rule, or to
# the library build, silently undoes that.
$(TEST_GPU_BIN): $(TEST_GPU_SOURCES) psx/dev/gpu.h
mkdir -p $(dir $@)
$(CC) -std=c11 -O2 -g -DUSE_HARDWARE -DPSXE_DIAG_STDIO_DISABLE -I. -Ipsx -Ifrontend $(SDL_CFLAGS) $(TEST_GPU_SOURCES) -lm -o $@
$(CC) -std=c11 -O2 -g -DUSE_HARDWARE -DPSXE_DIAG_STDIO_DISABLE -DARMSX_TEST_OFFSET_CENSUS -I. -Ipsx -Ifrontend $(SDL_CFLAGS) $(TEST_GPU_SOURCES) -lm -o $@
test-gpu: $(TEST_GPU_BIN)
./$(TEST_GPU_BIN)
-559
View File
File diff suppressed because it is too large Load Diff
-171
View File
@@ -538,67 +538,6 @@ static inline void psx_cpu_exception(psx_cpu_t* cpu, uint32_t cause) {
cpu->next_pc = cpu->pc + 4;
}
/* ---- packer trace (psx/cpu.h) --------------------------------------------------------- */
static psx_trace_call_t g_trace[PSX_TRACE_SLOTS];
static int g_trace_slot = -1; /* slot being recorded, -1 = idle */
const psx_trace_call_t* psx_trace_call(unsigned slot) {
return (slot < PSX_TRACE_SLOTS) ? &g_trace[slot] : NULL;
}
void psx_trace_reset(void) {
memset(g_trace, 0, sizeof(g_trace));
g_trace_slot = -1;
}
/* Called once per instruction, AFTER it has executed, so the register columns show its
result. Idle cost is the entry-PC compare in the caller. */
static void trace_step(psx_cpu_t* cpu) {
psx_trace_call_t* c;
if (g_trace_slot < 0) {
const uint32_t a0 = cpu->r[4];
if (cpu->saved_pc != PSX_TRACE_ENTRY || a0 < 3u || a0 > 5u)
return;
/* First call per argument only -- a later one would overwrite the pairing. */
if (g_trace[a0 - 3u].n)
return;
g_trace_slot = (int)(a0 - 3u);
c = &g_trace[g_trace_slot];
c->a0_in = a0;
c->ra_in = cpu->r[31];
c->s0_in = cpu->r[16];
c->s1_in = cpu->r[17];
}
c = &g_trace[g_trace_slot];
if (c->n < PSX_TRACE_STEPS) {
psx_trace_step_t* st = &c->step[c->n++];
st->pc = cpu->saved_pc;
st->opcode = cpu->opcode;
st->at = cpu->r[1];
st->v0 = cpu->r[2];
st->v1 = cpu->r[3];
st->a0 = cpu->r[4];
} else {
c->truncated = 1;
}
/* `jr ra` plus its delay slot ends the call; v0 is then the return value. */
if (cpu->opcode == 0x03e00008u && c->n > 1u) {
c->v0_out = cpu->r[2];
} else if (c->v0_out || c->truncated) {
c->v0_out = cpu->r[2];
g_trace_slot = -1;
}
}
void psx_cpu_cycle(psx_cpu_t* cpu) {
cpu->last_cycles = 0;
@@ -696,10 +635,6 @@ void psx_cpu_cycle(psx_cpu_t* cpu) {
cpu->total_cycles += cpu->last_cycles;
cpu->r[0] = 0;
/* Packer trace (psx/cpu.h). One compare while idle. */
if (g_trace_slot >= 0 || cpu->saved_pc == PSX_TRACE_ENTRY)
trace_step(cpu);
}
void psx_cpu_set_irq_pending(psx_cpu_t* cpu) {
@@ -1059,102 +994,6 @@ static inline void psx_cpu_i_lwr(psx_cpu_t* cpu) {
// );
}
/* ---- draw-environment store watch (psx/cpu.h) ---------------------------------------- */
#define STORE_WATCH_LO 0x005a250u
#define STORE_WATCH_HI 0x005a274u
static psx_store_watch_t g_sw_win[PSX_STORE_WATCH_WIN];
static psx_store_watch_t g_sw_val[PSX_STORE_WATCH_VAL];
static unsigned g_sw_win_total = 0;
static unsigned g_sw_val_total = 0;
unsigned psx_store_watch_window_total(void) { return g_sw_win_total; }
unsigned psx_store_watch_value_total(void) { return g_sw_val_total; }
unsigned psx_store_watch_window_kept(void) {
return (g_sw_win_total < PSX_STORE_WATCH_WIN) ? g_sw_win_total : PSX_STORE_WATCH_WIN;
}
unsigned psx_store_watch_value_kept(void) {
return (g_sw_val_total < PSX_STORE_WATCH_VAL) ? g_sw_val_total : PSX_STORE_WATCH_VAL;
}
/* Oldest-first over whatever the ring still holds, so a wrapped ring reads in order. */
const psx_store_watch_t* psx_store_watch_window(unsigned index) {
const unsigned kept = psx_store_watch_window_kept();
unsigned base;
if (index >= kept)
return NULL;
base = (g_sw_win_total >= PSX_STORE_WATCH_WIN)
? (g_sw_win_total % PSX_STORE_WATCH_WIN) : 0u;
return &g_sw_win[(base + index) % PSX_STORE_WATCH_WIN];
}
const psx_store_watch_t* psx_store_watch_value(unsigned index) {
const unsigned kept = psx_store_watch_value_kept();
unsigned base;
if (index >= kept)
return NULL;
base = (g_sw_val_total >= PSX_STORE_WATCH_VAL)
? (g_sw_val_total % PSX_STORE_WATCH_VAL) : 0u;
return &g_sw_val[(base + index) % PSX_STORE_WATCH_VAL];
}
void psx_store_watch_reset(void) {
g_sw_win_total = 0;
g_sw_val_total = 0;
}
/* Off the hot path by construction -- only reached once a store has already matched. The
code window is read through the bus; the store itself has not happened yet. */
static void store_watch_fill(psx_cpu_t* cpu, psx_store_watch_t* e,
uint32_t addr, uint32_t value, uint32_t width) {
unsigned i;
/* cpu->pc has advanced past the store; saved_pc is the instruction itself. */
e->pc = cpu->saved_pc;
e->opcode = cpu->opcode;
e->addr = addr;
e->value = value;
e->width = width;
e->base_reg = (cpu->opcode >> 21) & 0x1f;
e->src_reg = (cpu->opcode >> 16) & 0x1f;
e->base_val = cpu->r[e->base_reg];
e->src_val = cpu->r[e->src_reg];
for (i = 0; i < PSX_STORE_WATCH_CODE; i++)
e->code[i] = psx_bus_read32(cpu->bus, ((e->pc + (i * 4u)) - 16u) & 0x1ffffcu);
}
static void store_watch_maybe(psx_cpu_t* cpu, uint32_t addr, uint32_t value, uint32_t width) {
const uint32_t phys = addr & 0x1ffffcu;
/* The window is checked FIRST and lives in its own ring, so no amount of unrelated
traffic can evict the record this capture exists to obtain. */
if ((phys >= STORE_WATCH_LO) && (phys < STORE_WATCH_HI)) {
store_watch_fill(cpu, &g_sw_win[g_sw_win_total % PSX_STORE_WATCH_WIN],
addr, value, width);
g_sw_win_total++;
return;
}
/* E5 only, and only whole words. The E3/E4 triggers of the previous build fired on
ordinary data that happened to contain those bytes and consumed the budget. */
if ((width == 4u) && ((value & 0xff000000u) == 0xe5000000u)) {
store_watch_fill(cpu, &g_sw_val[g_sw_val_total % PSX_STORE_WATCH_VAL],
addr, value, width);
g_sw_val_total++;
}
}
static inline void psx_cpu_i_sb(psx_cpu_t* cpu) {
TRACE_M("sb");
@@ -1170,7 +1009,6 @@ static inline void psx_cpu_i_sb(psx_cpu_t* cpu) {
return;
}
store_watch_maybe(cpu, s + IMM16S, t, 1);
psx_bus_write8(cpu->bus, s + IMM16S, t);
}
@@ -1193,16 +1031,11 @@ static inline void psx_cpu_i_sh(psx_cpu_t* cpu) {
if (addr & 0x1) {
psx_cpu_exception(cpu, CAUSE_ADES);
} else {
store_watch_maybe(cpu, addr, t, 2);
psx_bus_write16(cpu->bus, addr, t);
}
}
static inline void psx_cpu_i_swl(psx_cpu_t* cpu) {
/* Unaligned stores are how a copied display list lands; the window trigger covers them
even though the partial value is not a whole command word. */
store_watch_maybe(cpu, cpu->r[S] + IMM16S, cpu->r[T], 4u);
TRACE_M("swl");
uint32_t s = cpu->r[S];
@@ -1247,15 +1080,11 @@ static inline void psx_cpu_i_sw(psx_cpu_t* cpu) {
if (psx_pgxp_active())
psx_pgxp_cpu_sw(addr, t, T);
store_watch_maybe(cpu, addr, t, 4);
psx_bus_write32(cpu->bus, addr, t);
}
}
static inline void psx_cpu_i_swr(psx_cpu_t* cpu) {
store_watch_maybe(cpu, cpu->r[S] + IMM16S, cpu->r[T], 4u);
TRACE_M("swr");
uint32_t s = cpu->r[S];
-93
View File
@@ -355,97 +355,4 @@ int psx_cpu_widescreen_hack(void);
#define GTEF_M1POVF 0x40000000
#define GTEF_ERRORF 0x80000000
/* ---- draw-environment store watch -----------------------------------------------------
ONE-SHOT diagnostic, always compiled, inert until read. Rebuilt after a first capture
filled its ring on irrelevant traffic before reaching the interesting store.
The question is narrow: a Xenogears battle sends a GP0 packet whose drawing-offset word
holds 0x038000 where the console produces 0x070000 -- the same row one bit position short.
Real hardware does not clip, so the value is ours. The packet has sat at 0x005a25c across
three capture sessions. Does anything in this emulator ever WRITE that word?
Two SEPARATE rings, so neither can crowd the other out:
window every store into 0x005a250-0x005a274 at any width, including SWL/SWR (a memcpy
of a display list is an unaligned copy, not a run of SW). This is the answer.
value 32-bit stores of an E5-prefixed word ANYWHERE else. This is the control: the
game demonstrably builds correct e5070000 / e5000000 packets, and their PCs are
what a wrong one has to be compared against.
Both keep the LAST N, not the first: the interesting store happens after frame setup, so
first-N is precisely the wrong end. Both also keep a monotonic total, so a ring that
wrapped is distinguishable from one that did not.
The E3/E4 value triggers are GONE. They fired on ordinary data bytes that happen to equal
0xe3 and ate the budget.
A ZERO window total is a real result, not a failed capture: it means the packet is static
or load-time data, nothing on the CPU store path builds it during the battle, and the CPU
line of inquiry is closed.
Cost when idle: one range compare on the store paths. */
#define PSX_STORE_WATCH_WIN 24
#define PSX_STORE_WATCH_VAL 8
#define PSX_STORE_WATCH_CODE 8
typedef struct {
uint32_t pc;
uint32_t opcode;
uint32_t addr;
uint32_t value;
uint32_t width;
uint32_t base_reg, base_val;
uint32_t src_reg, src_val;
uint32_t code[PSX_STORE_WATCH_CODE];
} psx_store_watch_t;
/* Monotonic totals -- how many stores MATCHED, whether or not the ring kept them. */
unsigned psx_store_watch_window_total(void);
unsigned psx_store_watch_value_total(void);
/* Entries held, newest last. Index is into the kept ring, not the total. */
unsigned psx_store_watch_window_kept(void);
unsigned psx_store_watch_value_kept(void);
const psx_store_watch_t* psx_store_watch_window(unsigned index);
const psx_store_watch_t* psx_store_watch_value(unsigned index);
void psx_store_watch_reset(void);
/* ---- packer trace ---------------------------------------------------------------------
The store watch named the site: a shared routine at 0x80046638 is called with the GP0
command index in a0 (3 = E3, 4 = E4, 5 = E5) and returns the coordinate payload, which the
caller ORs with the command byte and stores. It returns 0x038000 for BOTH a0=3 and a0=5,
where a0=5 must give 0x070000 -- Y at bit 11 rather than bit 10. On the other buffer
parity it returns 0 for both, which is why exactly half the frames are correct.
`shift-matrix` already cleared the shift instructions over all 32 amounts, so a wrong
shift AMOUNT reaching a correct shift is still live, as is an a0-indexed table load, a
mis-resolved branch on a0, or the jal/jr path. Reading which of those it is needs the
instruction sequence, not another aggregate.
So: trace the routine. One entry per distinct a0, first call only, so a0=3 and a0=5 come
back as two traces of the same code that can be diffed instruction by instruction --
a0=3 is the built-in control, since it produces the CORRECT payload from the same routine.
Cost: one compare against the entry PC per instruction while idle. */
#define PSX_TRACE_ENTRY 0x80046638u
#define PSX_TRACE_STEPS 96
#define PSX_TRACE_SLOTS 3 /* a0 = 3, 4, 5 */
typedef struct {
uint32_t pc, opcode;
uint32_t at, v0, v1, a0; /* r1, r2, r3, r4 AFTER the instruction executed */
} psx_trace_step_t;
typedef struct {
uint32_t a0_in, ra_in, s0_in, s1_in;
uint32_t v0_out;
uint32_t n;
uint32_t truncated;
psx_trace_step_t step[PSX_TRACE_STEPS];
} psx_trace_call_t;
const psx_trace_call_t* psx_trace_call(unsigned slot); /* slot 0..2 -> a0 3..5 */
void psx_trace_reset(void);
#endif
+3 -33
View File
@@ -171,9 +171,9 @@ void psx_dma_write8(psx_dma_t* dma, uint32_t offset, uint8_t value) {
case 0x76: dma_write_dicr(dma, ((uint32_t)value) << 16); break;
case 0x77: dma_write_dicr(dma, ((uint32_t)value) << 24); break;
default: {
log_fatal("Unhandled 8-bit DMA write at offset %08x (%02x)", offset, value);
//exit(1);
/* Not fatal, and consistent with psx_dma_write16/32 above: nothing here ends the
run, and log_fatal on a hot path is how the real errors get buried. */
log_error("Unhandled 8-bit DMA write at offset %08x (%02x)", offset, value);
} break;
}
}
@@ -251,9 +251,6 @@ void psx_dma_do_gpu_linked(psx_dma_t* dma) {
uint32_t hdr = psx_bus_read32(dma->bus, dma->gpu.madr);
uint32_t size = hdr >> 24;
uint32_t addr = dma->gpu.madr;
/* Instrumentation only: the address of the header word of the node being walked, so a
captured GP0(E5) can be reported with the packet it came out of. */
uint32_t node_addr = dma->gpu.madr;
int timeout = 16384;
@@ -270,26 +267,6 @@ void psx_dma_do_gpu_linked(psx_dma_t* dma) {
if (psx_pgxp_active())
psx_pgxp_note_gp0_word(addr);
/* Same note, unconditional and independent of PGXP: instrumentation needs the
source address in a normal (PGXP-off) session. One store per DMA word. */
psx_gpu_note_gp0_source(addr);
/* Instrumentation only, and only for GP0(E5) -- three words in a frame of tens of
thousands, so the bus reads below are noise. Everything else this costs is one
compare per DMA word. See psx_gpu_note_e5_context() in gpu.h for why. */
if ((cmd >> 24) == 0xe5u) {
uint32_t ctx[PSX_GPU_E5_CTX];
int i;
for (i = 0; i < PSX_GPU_E5_CTX; i++) {
const uint32_t a = (addr + (uint32_t)((i - 4) * 4)) & 0x1ffffc;
ctx[i] = psx_bus_read32(dma->bus, a);
}
psx_gpu_note_e5_context(addr, node_addr, hdr, ctx);
}
// Write to GP0
psx_bus_write32(dma->bus, 0x1f801810, cmd);
@@ -303,13 +280,8 @@ void psx_dma_do_gpu_linked(psx_dma_t* dma) {
if (addr == 0xffffff)
break;
node_addr = addr;
hdr = psx_bus_read32(dma->bus, addr);
size = hdr >> 24;
/* Instrumentation only -- this does NOT change the terminator test above. It records
whether the pointer just followed was one hardware would have stopped on. */
psx_gpu_note_dma_node(addr, hdr);
}
}
@@ -331,8 +303,6 @@ void psx_dma_do_gpu_request(psx_dma_t* dma) {
if (psx_pgxp_active())
psx_pgxp_note_gp0_word(dma->gpu.madr);
psx_gpu_note_gp0_source(dma->gpu.madr);
psx_bus_write32(dma->bus, 0x1f801810, data);
dma->gpu.madr += CHCR_STEP(gpu) ? -4 : 4;
+13 -116
View File
@@ -55,55 +55,11 @@ uint16_t gpu_to_bgr555(uint32_t color) {
// #define BGR555(c) gpu_to_bgr555(c)
/* Set by the DMA list walker just before it pushes a word into GP0; consumed by the next
psx_gpu_write32(). File-scope rather than a struct field because dma.c reaches the GPU
through the bus and has no psx_gpu_t to hand. */
static uint32_t g_gp0_src_addr = PSX_GPU_SRC_CPU;
void psx_gpu_note_gp0_source(uint32_t addr) {
g_gp0_src_addr = addr;
}
/* Accumulated across the frame, snapshotted at vblank. See the note in gpu.h. */
static uint32_t g_dma_nodes, g_dma_bit23, g_dma_first_bit23, g_dma_first_hdr;
static uint32_t g_e5ctx_src[4], g_e5ctx_node[4], g_e5ctx_hdr[4];
static uint32_t g_e5ctx_words[4][PSX_GPU_E5_CTX];
static unsigned g_e5ctx_n;
void psx_gpu_note_e5_context(uint32_t src, uint32_t node_addr, uint32_t hdr,
const uint32_t* ctx) {
unsigned i;
if (g_e5ctx_n >= 4u)
return;
g_e5ctx_src[g_e5ctx_n] = src;
g_e5ctx_node[g_e5ctx_n] = node_addr;
g_e5ctx_hdr[g_e5ctx_n] = hdr;
for (i = 0; i < PSX_GPU_E5_CTX; i++)
g_e5ctx_words[g_e5ctx_n][i] = ctx[i];
g_e5ctx_n++;
}
void psx_gpu_note_dma_node(uint32_t raw_next, uint32_t hdr) {
g_dma_nodes++;
if (raw_next & 0x800000u) {
if (!g_dma_bit23) {
g_dma_first_bit23 = raw_next;
g_dma_first_hdr = hdr;
}
g_dma_bit23++;
}
}
/* Always-on, marker-free. One 4-entry linear scan per primitive; see the note on off_hist
in gpu.h for why the offset in force DURING rasterization is the number that matters and
the last GP0(E5) of the frame is not. */
/* Buckets the drawing offset that was in force as each primitive rasterized. TEST BUILDS
ONLY -- see the note on the census fields in gpu.h for why this must not exist in a
shipped binary. Without ARMSX_TEST_OFFSET_CENSUS the call sites below expand to nothing
and the rasterizer entry points are exactly what they were before it was written. */
#ifdef ARMSX_TEST_OFFSET_CENSUS
static void gpu_offset_census(psx_gpu_t* gpu) {
const int16_t y = (int16_t)gpu->off_y;
unsigned i;
@@ -128,6 +84,9 @@ static void gpu_offset_census(psx_gpu_t* gpu) {
gpu->off_hist_n[3]++;
}
}
#else
#define gpu_offset_census(gpu) ((void)0)
#endif
int min3(int a, int b, int c) {
int m = (a <= b) ? a : b;
@@ -3239,7 +3198,6 @@ void psx_gpu_update_cmd(psx_gpu_t* gpu) {
gpu->texw_mx, gpu->texw_my, gpu->texw_ox, gpu->texw_oy);
} break;
case 0xe3: {
gpu->gp0_e3_raw = gpu->buf[0] & 0xffffff;
gpu->draw_x1 = (gpu->buf[0] >> 0 ) & 0x3ff;
gpu->draw_y1 = (gpu->buf[0] >> 10) & 0x1ff;
@@ -3249,7 +3207,6 @@ void psx_gpu_update_cmd(psx_gpu_t* gpu) {
gpu->draw_x1, gpu->draw_y1, gpu->draw_x2, gpu->draw_y2);
} break;
case 0xe4: {
gpu->gp0_e4_raw = gpu->buf[0] & 0xffffff;
gpu->draw_x2 = (gpu->buf[0] >> 0 ) & 0x3ff;
gpu->draw_y2 = (gpu->buf[0] >> 10) & 0x1ff;
@@ -3259,17 +3216,11 @@ void psx_gpu_update_cmd(psx_gpu_t* gpu) {
gpu->draw_x1, gpu->draw_y1, gpu->draw_x2, gpu->draw_y2);
} break;
case 0xe5: {
#ifdef ARMSX_TEST_OFFSET_CENSUS
/* Verbatim, before extraction. See the note on gp0_e5_raw in gpu.h. */
gpu->gp0_e5_raw = gpu->buf[0] & 0xffffff;
gpu->gp0_e5_count++;
if (gpu->gp0_e5_seq_n < 8) {
gpu->gp0_e5_seq[gpu->gp0_e5_seq_n] = gpu->buf[0] & 0xffffff;
gpu->gp0_e5_seq_prim[gpu->gp0_e5_seq_n] =
(uint16_t)((gpu->frame_prims > 0xffffu) ? 0xffffu : gpu->frame_prims);
gpu->gp0_e5_seq_src[gpu->gp0_e5_seq_n] = gpu->gp0_word_src;
gpu->gp0_e5_seq_n++;
}
#endif
gpu->off_x = ((int32_t)(((gpu->buf[0] >> 0 ) & 0x7ff) << 21)) >> 21;
gpu->off_y = ((int32_t)(((gpu->buf[0] >> 11) & 0x7ff) << 21)) >> 21;
@@ -3295,10 +3246,6 @@ void psx_gpu_update_cmd(psx_gpu_t* gpu) {
(gpu->accuracy_flags & PSX_GPU_ACCURACY_MASK_BIT) ? "yes" : "no");
} break;
default: {
/* Counted, not logged: an unbounded per-word log is what dma.c already taught this
project not to do. The count is what a desync looks like from outside. */
gpu->gp0_unknown++;
// log_set_quiet(0);
// log_fatal("Unhandled GP0(%02Xh)", gpu->buf[0] >> 24);
// log_set_quiet(1);
@@ -3312,10 +3259,6 @@ void psx_gpu_write32(psx_gpu_t* gpu, uint32_t offset, uint32_t value) {
switch (offset) {
// GP0
case 0x00: {
/* Latch and clear, so a CPU store cannot inherit the previous DMA word's note. */
gpu->gp0_word_src = g_gp0_src_addr;
g_gp0_src_addr = PSX_GPU_SRC_CPU;
switch (gpu->state) {
case GPU_STATE_RECV_CMD: {
gpu->buf_index = 0;
@@ -3380,23 +3323,6 @@ void psx_gpu_write32(psx_gpu_t* gpu, uint32_t offset, uint32_t value) {
uint8_t cmd = value >> 24;
switch (cmd) {
/* NOT implemented, only COUNTED — deliberately. GP1(00) resets the whole GPU
and GP1(01) resets the command buffer; both are no-ops here today, and this
arm changes nothing about that. It only records that one arrived, and
whether it arrived while this GPU was mid-command, which is the one
observable that separates "the command stream desynced" from every other
explanation for a corrupt picture. See the counters in gpu.h. */
case 0x00:
case 0x01: {
if (cmd == 0x00)
gpu->gp1_reset++;
else
gpu->gp1_fifo_reset++;
if (gpu->state != GPU_STATE_RECV_CMD)
gpu->gp1_reset_midcmd++;
} break;
// Display enable
case 0x03: {
/* GPU_HW_DEBUG()-only, like poly_quad above. */
@@ -3409,8 +3335,6 @@ void psx_gpu_write32(psx_gpu_t* gpu, uint32_t offset, uint32_t value) {
case 0x04: {
} break;
case 0x05: {
gpu->gp1_disp_start++;
gpu->gp1_05_raw = value & 0xffffff;
gpu->disp_x = value & 0x3ff;
gpu->disp_y = (value >> 10) & 0x1ff;
GPU_HW_DEBUG("gp1-display-start value=%08x disp=(%u,%u)", value, gpu->disp_x, gpu->disp_y);
@@ -3426,7 +3350,6 @@ void psx_gpu_write32(psx_gpu_t* gpu, uint32_t offset, uint32_t value) {
GPU_HW_DEBUG("gp1-display-range-v value=%08x disp_y=(%u,%u)", value, gpu->disp_y1, gpu->disp_y2);
} break;
case 0x08:
gpu->gp1_disp_mode++;
gpu->display_mode = value & 0xffffff;
GPU_HW_DEBUG(
"gp1-display-mode value=%08x display_mode=0x%08x video_standard=%s",
@@ -3552,37 +3475,11 @@ void gpu_hblank_event(psx_gpu_t* gpu) {
primitive dump opens and closes its single-frame capture. */
gpu_dump_vblank(gpu);
/* Snapshot-then-clear the offset history. The front-end samples AFTER this point in
the frame, so it has to read the frame that just ENDED, not the one starting. */
memcpy(gpu->gp0_e5_seq_last, gpu->gp0_e5_seq, sizeof(gpu->gp0_e5_seq));
memcpy(gpu->gp0_e5_seq_prim_last, gpu->gp0_e5_seq_prim, sizeof(gpu->gp0_e5_seq_prim));
memcpy(gpu->gp0_e5_seq_src_last, gpu->gp0_e5_seq_src, sizeof(gpu->gp0_e5_seq_src));
memcpy(gpu->off_hist_y_last, gpu->off_hist_y, sizeof(gpu->off_hist_y));
memcpy(gpu->off_hist_n_last, gpu->off_hist_n, sizeof(gpu->off_hist_n));
gpu->gp0_e5_seq_n_last = gpu->gp0_e5_seq_n;
gpu->off_hist_used_last = gpu->off_hist_used;
gpu->frame_prims_last = gpu->frame_prims;
memcpy(gpu->e5ctx_src_last, g_e5ctx_src, sizeof(g_e5ctx_src));
memcpy(gpu->e5ctx_node_last, g_e5ctx_node, sizeof(g_e5ctx_node));
memcpy(gpu->e5ctx_hdr_last, g_e5ctx_hdr, sizeof(g_e5ctx_hdr));
memcpy(gpu->e5ctx_words_last, g_e5ctx_words, sizeof(g_e5ctx_words));
gpu->e5ctx_n_last = (uint8_t)g_e5ctx_n;
g_e5ctx_n = 0;
gpu->dma_nodes_last = g_dma_nodes;
gpu->dma_bit23_last = g_dma_bit23;
gpu->dma_first_bit23_last = g_dma_first_bit23;
gpu->dma_first_hdr_last = g_dma_first_hdr;
g_dma_nodes = 0;
g_dma_bit23 = 0;
g_dma_first_bit23 = 0;
g_dma_first_hdr = 0;
gpu->gp0_e5_seq_n = 0;
#ifdef ARMSX_TEST_OFFSET_CENSUS
/* The offset census (see gpu_offset_census) is per-frame, so it is cleared here. */
gpu->off_hist_used = 0;
gpu->frame_prims = 0;
#endif
GPU_HW_DEBUG(
"vblank-start line=%d mode=%s gpustat=0x%08x display_mode=0x%08x draw=(%u,%u)-(%u,%u) disp=(%u,%u)-(%u,%u) offset=(%d,%d)",
+22 -134
View File
@@ -116,9 +116,6 @@ typedef struct rect_data_t {
struct psx_gpu_backend;
#endif
/* Words of RAM captured around a DMA-delivered GP0(E5); see psx_gpu_note_e5_context(). */
#define PSX_GPU_E5_CTX 8
struct psx_gpu_t {
uint32_t bus_delay;
uint32_t io_base, io_size;
@@ -225,109 +222,34 @@ struct psx_gpu_t {
uint32_t disp_x1, disp_x2;
uint32_t disp_y1, disp_y2;
/* ---- command-stream health counters ------------------------------------------------
/* ---- drawing-offset census: TEST BUILDS ONLY ------------------------------------------
Always on (an increment on a path that runs a handful of times per frame), never saved
in a state, and read only by instrumentation. They exist because "the picture is wrong
but the primitives are innocent" has two very different explanations, and the cheap one
to rule out is a DESYNCED COMMAND STREAM: this GPU implements neither GP1(00) "reset
GPU" nor GP1(01) "reset command buffer", so a game that issues either to resynchronise
after an aborted transfer leaves us parked in RECV_ARGS/RECV_DATA, eating the following
words as arguments. Everything after that is misparsed including GP0(E5), which is
exactly the register that has been observed carrying an impossible value.
Residue of the GPUINFO(5) investigation, retained ONLY because the
`offset-stream-integrity` case in tests/gpu_renderer_parity.c asserts on it and nothing
else can: it is what proves the GP0 intake neither swallowed, replayed nor invented a
GP0(E5) across a VRAM fill and an upload, and that every primitive rasterized under the
offset that was in force when it was submitted.
gp1_reset_midcmd is the smoking gun: a non-zero count means a reset arrived while this
GPU was mid-command and was ignored. Zero means the desync hypothesis is dead and the
corruption is elsewhere. */
uint32_t gp1_reset; /* GP1(00) seen (unimplemented) */
uint32_t gp1_fifo_reset; /* GP1(01) seen (unimplemented) */
uint32_t gp1_reset_midcmd; /* ... of those, the ones that arrived mid-command */
uint32_t gp1_disp_start; /* GP1(05) writes; several per frame = mid-frame scroll */
uint32_t gp1_disp_mode; /* GP1(08) writes */
uint32_t gp0_unknown; /* GP0 opcodes the dispatch does not recognise */
Compiled out unless ARMSX_TEST_OFFSET_CENSUS is defined, which ONLY the $(TEST_GPU_BIN)
rule in the Makefile does. The reason is not the handful of bytes: gpu_offset_census()
runs once per primitive, at the top of gpu_render_triangle() / _rect() / _flat_line(),
and at ~1800 primitives a frame a PGO run would shape branch layout and inlining in the
hottest function in the emulator around code that exists only to serve a test. In a
release build these fields do not exist and the three rasterizer entry points are
byte-for-byte what they were before the census was written.
/* ---- raw draw-state payloads ---------------------------------------------------------
The last 24-bit payload of each of GP0(E3/E4/E5) and GP1(05), verbatim, before any
field extraction. This is the `st=<RAWWORD>` the frame-summary banner has always
advertised and never actually emitted, and it exists for one question:
Xenogears sets a drawing offset of y=112 on exactly 10% of frames, against a drawing
area of (0,224)-(319,447), and every primitive on those frames is clipped. 112 is not
an arbitrary number. GP0(E5) packs Y at bit 11; GP1(05) packs Y at bit 10. The SAME
payload 0x038000 therefore reads as offset y=112 under one layout and display-start
y=224 under the other and 224 is exactly this game's second framebuffer row. A
factor of two between two registers whose only difference is one shift position is not
a coincidence worth arguing about from the extraction code; it is worth reading the
word off the device.
So: if e5_raw is 0x070000 on a bad frame we mis-extracted (and the extraction is
provably right, so it will not be); if it is 0x038000 the value arrived that way and
the defect is upstream of this GPU; if it equals gp1_05_raw on the same frame, the two
registers' payloads are being crossed. Three different bugs, one datum.
Cost: four stores on commands that occur about three times per frame. */
uint32_t gp0_e3_raw, gp0_e4_raw, gp0_e5_raw;
uint32_t gp1_05_raw;
Note that this changes sizeof(psx_gpu_t) between a test build and a release build. That
is safe because psx_gpu_save_state() serialises field by field rather than blitting the
struct, and because every gate compiles the whole core from source with its own flags
no object built with the define is ever linked against one built without it. */
#ifdef ARMSX_TEST_OFFSET_CENSUS
uint32_t gp0_e5_raw; /* last GP0(E5) payload, verbatim, before extraction */
uint32_t gp0_e5_count; /* GP0(E5) writes; >1 per frame means the offset moved */
/* ---- per-frame offset history ---------------------------------------------------------
The fields above record the LAST GP0(E5) of a frame, and a Xenogears battle frame turns
out to issue THREE. "The last one carried the wrong payload" and "the scene was drawn
with the wrong offset" are then completely different claims, and only the second one
explains lost pixels: if the game sets a good offset, draws 1600 primitives, and only
then writes a bad offset for a trailing pass, nothing is clipped and the once-per-frame
sample is simply looking at the wrong moment.
So two things are recorded per frame:
e5_seq every GP0(E5) payload in order, with the primitive index it landed between
off_hist the offset ACTUALLY IN FORCE as each primitive rasterized, bucketed
off_hist is the one that decides it. `224:1598, 112:3` means the frame drew correctly
and the anomaly is cosmetic; `112:1650` means every primitive was displaced and this is
the defect. Nothing else distinguishes those two, and they need opposite fixes.
Double-buffered: the accumulators are snapshotted into the _last copies at vblank and
then cleared, because the front-end samples AFTER the core's vblank and would otherwise
always read a freshly-zeroed frame.
Cost: one bucket search (4 entries, linear) per primitive. */
uint32_t gp0_e5_seq[8];
uint16_t gp0_e5_seq_prim[8];
/* Where each GP0(E5) word CAME FROM: the RAM address the DMA read it out of, or
PSX_GPU_SRC_CPU for a direct MMIO store. This is the datum that separates "the game's
display list really contains this word" from "something in this emulator produced it":
with a RAM address in hand the list can be dumped and read. */
uint32_t gp0_e5_seq_src[8];
uint8_t gp0_e5_seq_n;
int16_t off_hist_y[4];
uint32_t off_hist_n[4];
int16_t off_hist_y[4]; /* the offset actually in force as each primitive drew, */
uint32_t off_hist_n[4]; /* bucketed by Y, overflow folded into the last bucket */
uint8_t off_hist_used;
uint32_t frame_prims;
uint32_t gp0_word_src; /* source of the GP0 word being dispatched right now */
/* Linked-list walk census, snapshotted at vblank with the rest. bit23 counts
next-pointers that hardware would have treated as the end of the list and this
walker followed anyway; first_* keep the first such pointer and the header it led to. */
uint32_t dma_nodes_last, dma_bit23_last, dma_first_bit23_last, dma_first_hdr_last;
/* Up to four E5 words per frame with their surrounding RAM; double-buffered like the
rest, because the front-end samples after the core's vblank. */
uint32_t e5ctx_src[4], e5ctx_node[4], e5ctx_hdr[4];
uint32_t e5ctx_words[4][PSX_GPU_E5_CTX];
uint8_t e5ctx_n;
uint32_t e5ctx_src_last[4], e5ctx_node_last[4], e5ctx_hdr_last[4];
uint32_t e5ctx_words_last[4][PSX_GPU_E5_CTX];
uint8_t e5ctx_n_last;
uint32_t gp0_e5_seq_last[8];
uint16_t gp0_e5_seq_prim_last[8];
uint32_t gp0_e5_seq_src_last[8];
uint8_t gp0_e5_seq_n_last;
int16_t off_hist_y_last[4];
uint32_t off_hist_n_last[4];
uint8_t off_hist_used_last;
uint32_t frame_prims_last;
#endif
// Timing and IRQs
float cycles;
@@ -359,40 +281,6 @@ uint32_t psx_gpu_read32(psx_gpu_t*, uint32_t);
uint16_t psx_gpu_read16(psx_gpu_t*, uint32_t);
uint8_t psx_gpu_read8(psx_gpu_t*, uint32_t);
void psx_gpu_write32(psx_gpu_t*, uint32_t, uint32_t);
/* "The next GP0 word comes from this RAM address." Called by the DMA list walker
immediately before it pushes the word; consumed by the very next psx_gpu_write32(). A
plain store, unconditional, so it works with PGXP off (which is the shipping default and
the reason the existing PGXP note could not be used for this). */
#define PSX_GPU_SRC_CPU 0xffffffffu
void psx_gpu_note_gp0_source(uint32_t addr);
/* "The list walker has just followed a next-pointer to this node." `raw_next` is the
pointer BEFORE any masking, which is the whole point: PlayStation hardware ends a GPU
linked list when the next-pointer has bit 23 set, and psx_dma_do_gpu_linked() tests only
for an exact 0xffffff. If a game terminates with any other bit-23 value this walker keeps
going, and the inner loop's `& 0x1ffffc` folds the runaway pointer back into low RAM,
where unrelated data is then executed as GP0 commands.
That is not a theory about the observed Xenogears failure, it is arithmetic: a terminator
of 0x85a240 folds to a first word read at 0x005a244, which is exactly the address the
spurious GP0(E5) was measured coming from. This records whether it actually happens. */
void psx_gpu_note_dma_node(uint32_t raw_next, uint32_t hdr);
/* The RAM around a GP0(E5) word that arrived by DMA, captured by the list walker (which is
the only place that still has a bus to read with). `ctx` is eight words: the four
preceding the E5, the E5 itself, and three after.
This is the measurement that ends the ambiguity the source addresses could not. Every
other explanation is now excluded -- not a mis-extraction, not a FIFO desync, not
display-start crossover, and not a walker runaway (bit23 was measured at 0). What is left
is whether the words at 0x005a2xx are a legitimate GP0 packet the game submitted, or some
other structure we are reaching by a route nobody has found yet. Eight words of context
plus the node header that links there distinguishes those by inspection: a real packet has
recognisable opcodes in its top bytes and a plausible header word count; a texture, a
vertex buffer or a sound table does not. */
void psx_gpu_note_e5_context(uint32_t src, uint32_t node_addr, uint32_t hdr,
const uint32_t* ctx);
void psx_gpu_write16(psx_gpu_t*, uint32_t, uint16_t);
void psx_gpu_write8(psx_gpu_t*, uint32_t, uint8_t);
/* VRAM plus every GP0/GP1 latch and the scanline/dot-clock counters. The