mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
Strip session instrumentation for release; gate offset census behind ARMSX_TEST_OFFSET_CENSUS
This commit is contained in:
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user