WIP: stage production history gates pending quiet-host timing

This commit is contained in:
Matthew Stanley
2026-09-05 02:02:10 -07:00
parent fad429636d
commit df212a8693
9 changed files with 1168 additions and 31 deletions
+162
View File
@@ -251,3 +251,165 @@ checkpoint parity, multi-title/low-end hardware, moving/transition routes,
long-window frame consistency, clean-install and cross-platform release gates
remain open. Broader GPU/audio history and GL costs remain separate candidates;
the flat gameplay result does not justify focusing the campaign on memory.
## PACER-COST-001: attribute idle pacing CPU cost
Date: 2026-09-05. Verdict: baseline attribution only, no pacing change or
gameplay speed claim. Compile actual `frame_pacing.c` and `host_time.c` against
SDL3 with GCC 15.2.0 `-O2 -DPSX_SDL3`. The driver calls `frame_pacer_wait` 600
times at 59.94 Hz with no guest work, using `GetThreadTimes` for calling-thread
CPU time and SDL's performance counter for wall time. The first call anchors
without waiting, so 599 periods take approximately 9.9933 seconds.
| Run | Wall seconds | Thread CPU seconds |
| --- | --- | --- |
| 1 | 9.993298 | 0.812500 |
| 2 | 9.993297 | 0.859375 |
| 3 | 9.993297 | 0.765625 |
This is approximately 7.7-8.6% of one logical CPU spent on idle pacing on the
Ryzen 7 9800X3D, High Performance plan, unrestricted scheduling and inherited
affinity. The existing sleep decision stops sleeping below two milliseconds,
then spins until the deadline. This local cost makes the spin tail worth
measuring in gameplay; it does not establish an actual pacer bug or explain
exclusive wall-time phase attribution under a Job cap. No candidate was tested.
Agents held compilation during these runs; no continuous compiler-process or
thermal monitor was attached to this exploratory probe.
Local retained driver `.local/pacer-cost.c`, SHA256
`d98305437fb4e8c86769f31893e43bf63ade8568d761f522e0842817a92ac105`;
executable `.local/pacer-cost.exe`, SHA256
`7aa80698ca5d3a831d0d651601b3c5b2e8bc3665e2eec82f5e426c308eae7ddb`;
raw output `.local/pacer-cost-results.txt`, SHA256
`749fc5185fa512e3a00390f0022fa8939993763ca3b9f6e123ba76ff6654fea1`.
## HISTORY-002: isolated GPU and audio history candidates
Date: 2026-09-05. Correctness milestone; controlled resource/time comparison
pending. No gameplay gain accepted from the correctness runs below.
### Candidate boundaries
- `GP0-HISTORY-001`: compile the GP0 command-history allocation, recording and
diagnostic guest-stack scan only when debug tools are enabled. Production
history accessors remain linkable but report zero capacity/count and empty
dumps. Keep source tracking, opcode counters, WS census, functional overhang
detection, and all command execution unchanged. Removes a 104 MiB allocation
request; debug builds retain always-on history.
- `AUDIO-PCM-001`: compile out four PCM history arrays and their writes in
`PSX_NO_DEBUG_TOOLS` builds. Preserve every tap's head, nonzero/audible/peak
statistics, rates, event records and pump/underrun/mute counters. Production
WAV dump returns the existing error result without opening a file; debug
WAV history is unchanged. Actual GCC object BSS is 83,886,272 bytes with debug
history and 16,777,408 without it: exactly 64 MiB less backing capacity.
Both use the existing build switch, not a new runtime selector or lazy allocator.
Capacity is not equivalent to resident memory or a measured speed improvement.
Packed production audio tap counters may share cache lines between callback and
emulation threads; require the real bridge-audio path in performance checks.
### Focused checks
The GPU test compiles actual `gpu.c` and real headers with external renderer
stubs. It compares exact serialized GPU snapshots and the entire VRAM at eleven
checkpoints across environment, fill, upload, copy, polyline and NOP commands.
It also checks debug history count, copy source/stack pointer and polyline
header, and production unavailability. This tests parser/state behavior, not
the rendering correctness of a stubbed backend. Known pre-existing polyline
source attribution was not changed or silently redefined by the test.
The audio test compiles actual `audio_trace.c` in both modes. All four taps'
observed statistics/events compare equally, including a new event after ring
wrap, signed extremes, audible thresholds, rates and resets. Debug WAV headers
and sample bytes are checked for all taps and for wrap/clamped eviction;
production must not create the requested WAV. Review rejected the first test's
hard-coded output comparison and replaced it with real observations.
Both focused tests passed root execution. The 63 campaign/host/stall-report
tests passed, as did the memory-trace regression. CMake's registration guard
finds 122 test files registered. This is not a full cross-platform CTest pass.
### Isolated build provenance
B is the previous memory-guard baseline, C adds only the GP0 gate, and D adds
only the audio gate. All use identical local-only endpoint memory/restore/
renderer/audio witnesses and an optional checkpoint probe. Original B from
MEMORY-TRACE-001 is preserved. Source trees, exact options/commands/logs and
hashes are in `.local/ape-build-provenance/builds-bcd-manifest.json`, SHA256
`176ed3d54939fab3498ced5d1e3dd38566a9ef76e7e4cb23f5514a8d9a740530`.
| Variant | Executable SHA256 |
| --- | --- |
| B metrics | `f1310b19eb3633d81e01717f36ab60f4d6cd4d53aa3838adf0e614d10bbf4914` |
| C GP0 | `74063d9dadef152d023c0dead18eabde854773c4399f7f52f49713fb67315045` |
| D PCM | `e9f525296dfdb0be2a50596a1198bb9735be0371d21afa627cc6e08dfe05bd39` |
Release GCC 15.2.0, SDL3, static runtime, debug tools OFF, rewind ON, netplay OFF,
same donor generated game/BIOS and saved scene as MEMORY-TRACE-001. The stale
generated-BIOS warning persists. A default all-target build failed in an
unrelated donor title test on a long Windows dependency-file path; explicit
`psx-runtime` builds succeeded. No release qualification is claimed.
### Real OpenGL checkpoint comparison
Separate uncapped 20-second processes restore the same slot-1 v5 beach state.
All variants report active OpenGL (`gl_active=1`, `gr_backend=1`), completed
restore (`generation=1,pending=0,last_ok=1,last_op=load,last_slot=1`), and active
non-legacy bridge output with advancing host-tap frames. The local-only probe
writes raw boot-state blobs at frames 120, 121 and 420, plus CPU timing sidecars.
For **both C and D versus B, every blob and sidecar is byte-identical**.
| Frame | Raw guest snapshot SHA256 (all B/C/D) |
| --- | --- |
| 120 | `505f7d94c5c7e518e099669ed1623296d9210abc30eab4bc907956e91ff652c6` |
| 121 | `1e0b19c3a913733aba9bb403b1e01a1c9d309cc919e3accd9c52f3d341d3429b` |
| 420 | `55ebbe6a36ac2674952a4672575dfb619547ffec0a059f9a277bb955b9d074e2` |
Each blob is 3,685,569 bytes. The existing save format covers CPU registers,
RAM, scratchpad, VRAM, SPU RAM and serialized device/IRQ/clock/cache/dirty state.
The 411-byte sidecars add multiply/divide and GTE completion timestamps, all 33
read-absorb bytes and load-timing scalars. CPU function pointers, host audio
queues, SDL/pacer/presentation histories and diagnostics are excluded. This is
one scene's checkpoint evidence, not exhaustive state/restore or multi-title
qualification. Full campaign correctness gates remain open.
Evidence: `.local/history-correctness-002/report.json`, SHA256
`7feb56b0f0119688e460ff46b027d3aaf9549021e996a935830e26a6154b21f2`;
driver `.local/run_history_checks.py`, SHA256
`f6f29b505225fc0ff8688d9a86921be60590a81288980c5fd7d4c8b56961d0f4`.
Raw game-derived snapshots remain local and uncommitted.
The first attempt, `.local/history-correctness-001`, failed closed because the
staged checkpoint hook was absent despite an earlier build report. Explicit
source and binary-string checks plus forced rebuilds corrected the artifact.
That failed attempt is retained, not counted as successful state comparison.
Checkpoint saves can publish deferred cycles and force GL readback. These runs
also overlapped unrelated builds, so **none of their timing or memory values is
acceptance evidence**. The timed runner explicitly unsets checkpoint capture.
The preregistered primary metric is exact-frame-420 process PrivateUsage; the
secondary metric is frames-120:420 wall time, with three interleaved pairs for
each independent candidate and a validated 25%-one-core Job cap. It requires
stable input/artifact hashes, active OpenGL/audio and completed restore, and
aborts on compiler contamination or missing/nonfinite metrics.
### Timing gate outcome and disposition
The benchmark agent monitored the host for ten minutes without finding 30
consecutive seconds free of compiler/benchmark activity. Concurrent SNES builds
included Mega Man X and Super Mario Kart. No timed candidate process was
launched and `.local/game-gp0-memory-ab` was not created. Unrelated work was
not stopped. The power plan remained High performance before and after;
thermal state was not measured.
The prepared twelve-run driver is `.local/run_game_gpu_memory_ab.py`, SHA256
`ce01b28c627dd5a8d0abe3217a49e2fd1d576fc7128153de502487f69a96f64b`.
It includes `cc.exe` in compiler-contamination detection. Resume the paired
campaign in a quiet host window before making retention or speedup claims.
Disposition: committed candidate implementation in the isolated performance
worktree, **pending performance acceptance**, not release qualification. The
104 MiB GP0 allocation request and 64 MiB PCM static-storage capacity removals
are established; resident-memory savings and gameplay speedups are not.
Focused parity tests and real OpenGL checkpoints passed, but the broader
correctness matrix and the quiet-host comparisons remain open.
+57 -3
View File
@@ -1,11 +1,18 @@
# Runtime Audit
Source audit for the runtime burndown, reviewed against the current branch.
The memory-trace candidate has now been implemented and measured; other rows
remain source hypotheses, not measured bottlenecks. See
Source audit for the runtime burndown. The matrix records the initial source
disposition; follow-up implementation and qualification are noted below.
The memory-trace candidate has been implemented and measured. See
[MEMORY-TRACE-001](EXPERIMENTS.md#memory-trace-001-remove-empty-production-write-trace-calls)
for results and limitations. Paths are repository-relative.
Follow-up: [HISTORY-002](EXPERIMENTS.md#history-002-isolated-gpu-and-audio-history-candidates)
implements production-only GP0 history and audio PCM-storage gates. Focused
tests and real OpenGL guest/CPU-timing checkpoints pass; controlled resource
and time comparisons remain pending. No census, GL hold-copy, pacing, threading
or Vulkan behavior has changed. [PACER-COST-001](EXPERIMENTS.md#pacer-cost-001-attribute-idle-pacing-cpu-cost)
provides idle-pacer CPU attribution, not a speedup claim.
## Disposition Matrix
| Area | Current disposition | Evidence | Candidate / next check | Correctness guardrails |
@@ -56,3 +63,50 @@ Known baseline structural-test failures are not caused by these candidates:
Root verified the same failures against immutable provenance under
`.local/ape-build-provenance/framework-8ec6498d/...`; treat them as validation
gaps before using those tests as acceptance for write-call work.
## Vulkan Qualification Audit
Read-only follow-up, 2026-09-05. No Vulkan build/run or performance claim.
The backend is substantially implemented: its vtable wires primitive rendering,
textures, masks, PGXP, transfers and native-wide callbacks (`gpu_vk_renderer.c`,
`vkb_backend` near line 3273). Its introductory phase-status comment is stale.
- `PSX_ENABLE_VULKAN=ON` is not proof of an active renderer. CMake only defines
`PSX_HAVE_VULKAN` after header/tool checks; otherwise `gpu_vk_renderer.c:35`
compiles inert stubs. Context initialization can also fail and select software
(`main.cpp:13623`). A real qualification run must witness the active backend.
- Readback is implemented but can force full VRAM pack/copy/wait through
`ensure_cpu` (`gpu_vk_renderer.c:2308`). It is not the normal 15-bit GPU-direct
present path. Missing evidence: mixed-command readback parity and counts
proving ordinary presentation does not unexpectedly enter this path.
- Native-wide presentation exists, but Vulkan omits the vtable's
`wide_dump_full` callback. `gr_wide_supported()` only checks
`render_wide_display`, so presentation support does not imply complete
diagnostic capture support (`gpu_render.c:165`, `gpu_render.h:197`).
- Netplay explicitly forces a Vulkan request to software because the present
path is not CPU-authoritative (`main.cpp:13075`). Preserve this correctness
guard; it is not an OpenGL optimization.
- Existing Vulkan tests cover source patterns and an upload-alignment helper,
not a live context drawing and comparing pixels. Required follow-up is a
backend matrix for primitive order, textures, blending, masks, feedback,
transfers, native-wide, depth24, capture, and initialization/fallback cases.
These findings justify a separate qualification track, not a recommendation to
switch users away from OpenGL or a conclusion that Vulkan is unimplemented.
## Local CPU Attribution Tools
Read-only inventory, 2026-09-05: WPR, xperf and WPA are installed, but the
attempted WPR CPU profile failed with policy error `0xc5585011`. PATH and
known-install-path checks found no AMD uProf, Intel VTune, Very Sleepy or
samply. This is a bounded inventory, not proof that no profiler exists anywhere
on the machine. No installation, elevation or policy change was attempted.
MinGW `gprof` is available, but requires an instrumented `-pg` rebuild and
cannot faithfully sample the existing Release artifact. `perf_host.py` supplies
whole-process CPU time, not stacks. Campaign phase counters are wall time, not
exclusive CPU time. `host_thread_probe.py` can suspend a same-user thread and
inspect its context/stack; repeated captures could establish coarse thread
stack occupancy with symbols, but are perturbing and must not be reported as
per-function CPU percentages. Cross-check such leads with controlled narrow
A/B experiments until a statistical CPU profiler can be used.
+10 -1
View File
@@ -507,7 +507,16 @@ if(BUILD_TESTING)
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC
AND NOT CMAKE_CROSSCOMPILING)
add_test(NAME audio_trace_pcm_guard_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_audio_trace_pcm_guard.py
--cc ${CMAKE_C_COMPILER})
add_test(NAME gpu_gp0_history_guard_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gpu_gp0_history_guard.py
--cc ${CMAKE_C_COMPILER})
add_test(NAME memory_debug_trace_guard_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_memory_debug_trace_guard.py
+11 -7
View File
@@ -1,10 +1,10 @@
/*
* audio_trace.h - always-on audio observability rings (PCM taps + event ring).
* audio_trace.h - audio health counters, event ring and debug PCM history.
*
* Port of the snesrecomp audio-campaign measurement harness (audio_trace.{c,h},
* commit 67ead27 lineage) to the PSX pipeline, following the cross-system tap
* model in F:/Projects/_audio_round2/ROUND2_PLAN.md: always-on ring buffers
* recording every sample/event from process start, dumped AFTER the fact via
* model in F:/Projects/_audio_round2/ROUND2_PLAN.md: debug-tool builds keep
* always-on ring buffers recording from process start, dumped AFTER the fact via
* TCP debug commands — never arm-then-reproduce.
*
* Taps (PSX pipeline):
@@ -21,9 +21,11 @@
* output — giving symmetric `audio_*` commands on ports 4370 and 4380.
*
* Threading: each tap has a single writer. Readers (debug-server thread)
* take a stable [oldest, head) snapshot; the PCM rings hold ~95 s at 44100,
* so a dump cannot be lapped mid-read in practice. Indices are C11 atomics
* (release on publish, acquire on read).
* take a stable [oldest, head) snapshot; in debug-tool builds the PCM rings
* hold ~95 s at 44100, so a dump cannot be lapped mid-read in practice.
* Production PSX_NO_DEBUG_TOOLS builds keep counters/events but compile out
* PCM history and WAV dumps. Indices are C11 atomics (release on publish,
* acquire on read).
*/
#ifndef PSX_AUDIO_TRACE_H
#define PSX_AUDIO_TRACE_H
@@ -118,7 +120,9 @@ uint32_t audio_trace_events_get(AudioTraceEvent *out, uint32_t max);
/* Write a tap slice as a stereo s16 WAV at the tap's rate. start=-1,count=0
* dumps the whole currently-buffered ring. `start`/`count` are absolute frame
* indices on the tap's production timeline. Returns frames written, or -1 on
* error (unwritable path / empty ring / slice already evicted). */
* error (unwritable path / empty ring / slice already evicted). In
* PSX_NO_DEBUG_TOOLS production builds, PCM history is not present and this
* returns -1 without creating/opening `path`. */
int64_t audio_trace_dump_wav(int tap, const char *path,
int64_t start, uint64_t count);
+5 -4
View File
@@ -102,11 +102,12 @@ typedef struct {
void gpu_arm_shaded_quad_capture(void);
int gpu_get_shaded_quad_capture(const GpuSqCapEntry** out);
/* Per-frame GP0 command ring (always-on; query via debug server).
* Each entry records the GP0 command header + up to 6 payload words
* (longer commands like 0x3C shaded textured quad are truncated to 6).
/* Per-frame GP0 command ring (always-on in debug-tool builds).
* Each entry records up to GPU_GP0_RING_MAX_WORDS command words, including
* the header; longer commands are truncated.
* Stamped with the s_frame_count value at issue time so a debug
* client can pull all commands for any frame in the ring window. */
* client can pull all commands for any frame in the ring window.
* PSX_NO_DEBUG_TOOLS builds report zero total/capacity and empty dumps/spans. */
#define GPU_GP0_RING_MAX_WORDS 12
typedef struct {
uint32_t frame;
+22 -5
View File
@@ -1,11 +1,10 @@
/*
* audio_trace.c - always-on audio observability rings. See audio_trace.h.
*
* Memory: 3 PCM taps x 2^22 stereo frames x 4 bytes = 48 MiB static, plus a
* 2^19-entry event ring (16 MiB). Static BSS, zero cost until touched; the
* recording hot path is a memcpy per audio block and is compiled into
* Release/production builds too (always-on ring-buffer discipline — probes
* query history, they never arm recording).
* Debug-tool builds keep 4 PCM taps x 2^22 stereo frames x 4 bytes = 64 MiB
* static history, plus a 2^19-entry event ring (16 MiB). Production
* PSX_NO_DEBUG_TOOLS builds retain counters/events but compile out PCM history
* storage and WAV dumps.
*/
#include "audio_trace.h"
@@ -14,8 +13,10 @@
#include <string.h>
/* 2^22 frames @ 44100 ~= 95 s per tap. Power of two so wrap is a mask. */
#ifndef PSX_NO_DEBUG_TOOLS
#define PCM_RING_FRAMES (1u << 22)
#define PCM_RING_MASK (PCM_RING_FRAMES - 1u)
#endif
#define EV_RING_CAP (1u << 19)
#define EV_RING_MASK (EV_RING_CAP - 1u)
@@ -26,7 +27,9 @@
#define AUDIBLE_ABS 256
typedef struct {
#ifndef PSX_NO_DEBUG_TOOLS
int16_t pcm[PCM_RING_FRAMES * 2];
#endif
_Atomic uint64_t head; /* frames ever written; ring pos = head & MASK */
uint64_t nonzero;
uint64_t audible;
@@ -98,9 +101,11 @@ void audio_trace_pcm(int tap, const int16_t *stereo, int frames)
for (int f = 0; f < frames; f++) {
int16_t l = stereo[f * 2 + 0];
int16_t r = stereo[f * 2 + 1];
#ifndef PSX_NO_DEBUG_TOOLS
uint32_t pos = (uint32_t)((head + (uint64_t)f) & PCM_RING_MASK);
t->pcm[pos * 2 + 0] = l;
t->pcm[pos * 2 + 1] = r;
#endif
int32_t al = l < 0 ? -(int32_t)l : l;
int32_t ar = r < 0 ? -(int32_t)r : r;
int32_t a = al > ar ? al : ar;
@@ -187,6 +192,7 @@ uint32_t audio_trace_events_get(AudioTraceEvent *out, uint32_t max)
/* ---- WAV dump --------------------------------------------------------- */
#ifndef PSX_NO_DEBUG_TOOLS
static void wav_write_header(FILE *fp, uint32_t data_bytes, uint32_t rate)
{
uint8_t h[44];
@@ -261,3 +267,14 @@ int64_t audio_trace_dump_wav(int tap, const char *path,
fclose(fp);
return (int64_t)count;
}
#else
int64_t audio_trace_dump_wav(int tap, const char *path,
int64_t start, uint64_t count)
{
(void)tap;
(void)path;
(void)start;
(void)count;
return -1;
}
#endif
+30 -11
View File
@@ -4919,15 +4919,14 @@ static uint32_t gp0_opcode_count[256];
uint32_t gpu_get_opcode_count(uint8_t op) { return gp0_opcode_count[op]; }
/* ---- Per-frame GP0 command ring (always-on, queried via debug server) ---- */
/* We record every GP0 command (header + up to 6 payload words) with the
* frame number it was issued in. Per CLAUDE.md ring-buffer rule: capture
* is continuous and observers query a window of interest later, not arm-
* then-record. ~34 MB at 1M entries. Polyline / long commands get the
* first 6 payload words; that's enough for the header + first vertex pair
* + first uv/color word for diagnosing per-primitive state. */
/* ---- Per-frame GP0 command ring (debug tools only) -----------------------
* We record every GP0 command with the frame number it was issued in. Polyline
* / long commands get the current public word cap, enough for diagnosing
* per-primitive state. Production builds leave the public accessors linkable
* but report an unavailable/empty capture. */
extern uint64_t s_frame_count; /* defined in debug_server.c */
#ifndef PSX_NO_DEBUG_TOOLS
extern uint32_t g_debug_last_store_pc; /* defined in debug_server.c */
extern uint32_t g_debug_current_func_addr; /* defined in debug_server.c */
uint32_t debug_guest_ra(void); /* accessor in debug_server.c (guest $ra) */
@@ -5004,10 +5003,6 @@ uint64_t gpu_gp0_ring_total(void) { return gp0_ring_seq; }
uint32_t gpu_gp0_ring_capacity(void) { return GP0_RING_CAP; }
uint32_t gpu_gp0_ring_max_words(void){ return GPU_GP0_RING_MAX_WORDS; }
void gpu_set_gp0_source(uint32_t addr) {
gp0_next_source_addr = addr;
}
/* Fill `out[0..max_out-1]` with entries from the requested frame; returns
* count. Walks from oldest in-buffer to newest so iteration order matches
* draw order within a frame. */
@@ -5038,6 +5033,30 @@ void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) {
if (out_oldest) *out_oldest = gp0_ring[start].frame;
if (out_newest) *out_newest = gp0_ring[newest_idx].frame;
}
#else
static void gp0_ring_record(const uint32_t *words, int n) {
(void)words;
(void)n;
}
uint64_t gpu_gp0_ring_total(void) { return 0; }
uint32_t gpu_gp0_ring_capacity(void) { return 0; }
uint32_t gpu_gp0_ring_max_words(void) { return GPU_GP0_RING_MAX_WORDS; }
int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out) {
(void)frame;
(void)out;
(void)max_out;
return 0;
}
void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) {
if (out_oldest) *out_oldest = 0;
if (out_newest) *out_newest = 0;
}
#endif
void gpu_set_gp0_source(uint32_t addr) {
gp0_next_source_addr = addr;
}
/* ---- Draw census ring (ALWAYS-ON) -----------------------------------------
* Every drawn primitive (GP0 opcode 0x20-0x7F) records: frame, the prim's DMA
+377
View File
@@ -0,0 +1,377 @@
#!/usr/bin/env python3
"""Build actual audio_trace.c in debug and PSX_NO_DEBUG_TOOLS modes."""
from __future__ import annotations
import argparse
import pathlib
import shutil
import subprocess
import sys
import tempfile
ROOT = pathlib.Path(__file__).resolve().parents[2]
DEFAULT_GCC = pathlib.Path(r"C:\msys64\mingw64\bin\gcc.exe")
DRIVER_C = r"""
#include "audio_trace.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PCM_RING_FRAMES (1u << 22)
static void fail(const char *msg) {
fprintf(stderr, "FAIL: %s\n", msg);
exit(1);
}
static uint32_t rd32(const unsigned char *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static int16_t rd16s(const unsigned char *p) {
uint16_t u = (uint16_t)p[0] | (uint16_t)((uint16_t)p[1] << 8);
return (int16_t)u;
}
static void push_small_taps(void) {
int16_t t0[] = {
0, 0,
1, -1,
256, -256,
257, -257,
32767, -32768,
};
int16_t t1[] = { -300, 0, 0, 300, 0, 0 };
int16_t t2[] = { 42, -42, 512, -512 };
int16_t t3[] = { 0, 0, 999, -999, -32768, 32767 };
audio_trace_pcm(AUDIO_TAP_SPU_OUT, t0, 5);
audio_trace_pcm(AUDIO_TAP_CD_IN, t1, 3);
audio_trace_pcm(AUDIO_TAP_HOST, t2, 2);
audio_trace_pcm(AUDIO_TAP_VOICES, t3, 3);
}
static void check_small_stats(void) {
AudioTraceStats st;
audio_trace_get_stats(&st);
if (st.tap_frames[0] != 5 || st.tap_nonzero[0] != 4 ||
st.tap_audible[0] != 2 || st.tap_peak[0] != 32768)
fail("tap0 stats");
if (st.tap_frames[1] != 3 || st.tap_nonzero[1] != 2 ||
st.tap_audible[1] != 2 || st.tap_peak[1] != 300)
fail("tap1 stats");
if (st.tap_frames[2] != 2 || st.tap_nonzero[2] != 2 ||
st.tap_audible[2] != 1 || st.tap_peak[2] != 512)
fail("tap2 stats");
if (st.tap_frames[3] != 3 || st.tap_nonzero[3] != 2 ||
st.tap_audible[3] != 2 || st.tap_peak[3] != 32768)
fail("tap3 stats");
}
static void print_stats(const char *label) {
AudioTraceStats st;
audio_trace_get_stats(&st);
printf("%s", label);
for (int t = 0; t < AUDIO_TAP_COUNT; t++) {
printf(" tap%d=%llu/%llu/%llu/%d/%u",
t,
(unsigned long long)st.tap_frames[t],
(unsigned long long)st.tap_nonzero[t],
(unsigned long long)st.tap_audible[t],
st.tap_peak[t],
audio_trace_tap_rate(t));
}
printf(" pump=%llu/%llu/%llu/%u/%u/%llu/%llu events=%llu total=%llu\n",
(unsigned long long)st.pump_calls,
(unsigned long long)st.pump_skips,
(unsigned long long)st.underruns,
st.queue_hiwater,
st.queue_lowater,
(unsigned long long)st.mute_events,
(unsigned long long)st.unmute_events,
(unsigned long long)st.events_total,
(unsigned long long)audio_trace_events_total());
}
static void push_events(void) {
audio_trace_note_frame(77);
audio_trace_event(AUDIO_EV_RENDER, 2048, 1234);
audio_trace_event(AUDIO_EV_PUMP_SKIP, 222, 0);
audio_trace_event(AUDIO_EV_UNDERRUN, 3, 1);
audio_trace_event(AUDIO_EV_MUTE, 64, 0);
audio_trace_event(AUDIO_EV_UNMUTE, 128, 0);
audio_trace_event(AUDIO_EV_CD_PUSH, 0xABCDEF01u, 17);
}
static void check_events(void) {
AudioTraceStats st;
AudioTraceEvent ev[8];
audio_trace_get_stats(&st);
if (st.pump_calls != 1 || st.pump_skips != 1 || st.underruns != 1 ||
st.mute_events != 1 || st.unmute_events != 1 ||
st.queue_hiwater != 1234 || st.queue_lowater != 1234 ||
st.events_total != 6)
fail("event stats");
if (audio_trace_events_total() != 6) fail("event total");
if (audio_trace_events_get(ev, 8) != 6) fail("event get count");
if (ev[0].seq != 0 || ev[0].sample_idx != 5 || ev[0].frame != 77 ||
ev[0].kind != AUDIO_EV_RENDER || ev[0].a != 2048 || ev[0].b != 1234)
fail("render event payload");
if (ev[5].kind != AUDIO_EV_CD_PUSH || ev[5].a != 0xABCDEF01u ||
ev[5].b != 17)
fail("cd event payload");
}
static void check_post_wrap_event(uint64_t expected_sample_idx) {
AudioTraceStats st;
AudioTraceEvent ev[8];
audio_trace_note_frame(88);
audio_trace_event(AUDIO_EV_DMA_READ, 12, 0x12345678u);
audio_trace_get_stats(&st);
if (st.events_total != 7 || audio_trace_events_total() != 7)
fail("post-wrap event count");
if (audio_trace_events_get(ev, 8) != 7) fail("post-wrap event get count");
if (ev[6].seq != 6 || ev[6].sample_idx != expected_sample_idx ||
ev[6].frame != 88 || ev[6].kind != AUDIO_EV_DMA_READ ||
ev[6].a != 12 || ev[6].b != 0x12345678u)
fail("post-wrap event payload");
}
static void print_events(const char *label) {
AudioTraceEvent ev[8];
uint32_t got = audio_trace_events_get(ev, 8);
printf("%s count=%u", label, got);
for (uint32_t i = 0; i < got; i++) {
printf(" e%u=%llu/%llu/%u/%u/%u/%u",
i,
(unsigned long long)ev[i].seq,
(unsigned long long)ev[i].sample_idx,
ev[i].frame,
ev[i].kind,
ev[i].a,
ev[i].b);
}
printf("\n");
}
static int file_exists(const char *path) {
FILE *fp = fopen(path, "rb");
if (!fp) return 0;
fclose(fp);
return 1;
}
static void push_wrap_tail(void) {
int16_t frame[2];
for (uint32_t i = 0; i < PCM_RING_FRAMES - 5u; i++) {
frame[0] = (int16_t)(i & 0x7FFFu);
frame[1] = (int16_t)(-(int32_t)(i & 0x7FFFu));
audio_trace_pcm(AUDIO_TAP_SPU_OUT, frame, 1);
}
for (int i = 0; i < 4; i++) {
frame[0] = (int16_t)(1000 + i);
frame[1] = (int16_t)(-2000 - i);
audio_trace_pcm(AUDIO_TAP_SPU_OUT, frame, 1);
}
}
static const int16_t *tap_expected(int tap, uint32_t *frames) {
static const int16_t t0[] = {
0, 0, 1, -1, 256, -256, 257, -257, 32767, -32768,
};
static const int16_t t1[] = { -300, 0, 0, 300, 0, 0 };
static const int16_t t2[] = { 42, -42, 512, -512 };
static const int16_t t3[] = { 0, 0, 999, -999, -32768, 32767 };
switch (tap) {
case AUDIO_TAP_SPU_OUT: *frames = 5; return t0;
case AUDIO_TAP_CD_IN: *frames = 3; return t1;
case AUDIO_TAP_HOST: *frames = 2; return t2;
default: *frames = 3; return t3;
}
}
static void check_wav_samples(const char *path, uint32_t rate,
const int16_t *expected, uint32_t frames) {
FILE *fp = fopen(path, "rb");
if (!fp) fail("wav open");
unsigned char data[44 + 64];
size_t want = 44u + (size_t)frames * 4u;
if (want > sizeof(data)) fail("wav test buffer too small");
size_t n = fread(data, 1, want, fp);
fclose(fp);
if (n != want) fail("wav size");
if (memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WAVEfmt ", 8) != 0 ||
memcmp(data + 36, "data", 4) != 0)
fail("wav tags");
if (rd32(data + 24) != rate) fail("wav rate");
if (rd32(data + 40) != frames * 4u) fail("wav data bytes");
for (uint32_t i = 0; i < frames * 2u; i++) {
if (rd16s(data + 44u + i * 2u) != expected[i])
fail("wav data");
}
}
static void check_short_wavs(const char *base_path, int production) {
char path[512];
for (int tap = 0; tap < AUDIO_TAP_COUNT; tap++) {
uint32_t frames = 0;
const int16_t *expected = tap_expected(tap, &frames);
snprintf(path, sizeof(path), "%s_tap%d.wav", base_path, tap);
remove(path);
int64_t wrote = audio_trace_dump_wav(tap, path, 0, frames);
if (production) {
if (wrote != -1) fail("production short wav should be unavailable");
if (file_exists(path)) fail("production short wav created file");
} else {
if (wrote != (int64_t)frames) fail("debug short wav frame count");
check_wav_samples(path, audio_trace_tap_rate(tap), expected, frames);
}
}
}
int main(int argc, char **argv) {
if (argc != 3) fail("usage");
const char *path = argv[1];
int production = strcmp(argv[2], "production") == 0;
audio_trace_init();
audio_trace_set_tap_rate(AUDIO_TAP_SPU_OUT, 48000);
audio_trace_set_tap_rate(AUDIO_TAP_CD_IN, 44101);
audio_trace_set_tap_rate(AUDIO_TAP_HOST, 32000);
audio_trace_set_tap_rate(AUDIO_TAP_VOICES, 22050);
push_small_taps();
check_small_stats();
print_stats("small");
push_events();
check_events();
print_events("events");
check_short_wavs(path, production);
push_wrap_tail();
AudioTraceStats st;
audio_trace_get_stats(&st);
if (st.tap_frames[AUDIO_TAP_SPU_OUT] != PCM_RING_FRAMES + 4ull)
fail("wrapped tap frame total");
check_post_wrap_event(st.tap_frames[AUDIO_TAP_SPU_OUT]);
print_stats("wrap");
print_events("events_after_wrap");
int64_t wrote = audio_trace_dump_wav(AUDIO_TAP_SPU_OUT, path,
(int64_t)PCM_RING_FRAMES, 4);
if (production) {
if (wrote != -1) fail("production wav should be unavailable");
if (file_exists(path)) fail("production wav created file");
} else {
int16_t wrapped[] = {
1000, -2000, 1001, -2001, 1002, -2002, 1003, -2003,
};
if (wrote != 4) fail("debug wav frame count");
check_wav_samples(path, 48000u, wrapped, 4);
{
int16_t oldest[] = { 32767, -32768 };
if (audio_trace_dump_wav(AUDIO_TAP_SPU_OUT, path, 0, 1) != 1)
fail("debug evicted prefix should clamp");
check_wav_samples(path, 48000u, oldest, 1);
}
}
audio_trace_init();
audio_trace_get_stats(&st);
for (int tap = 0; tap < AUDIO_TAP_COUNT; tap++) {
if (st.tap_frames[tap] || st.tap_nonzero[tap] || st.tap_audible[tap] ||
st.tap_peak[tap] || audio_trace_tap_total(tap))
fail("reset tap");
}
if (st.events_total || st.pump_calls || st.pump_skips || st.underruns ||
st.queue_hiwater || st.queue_lowater || st.mute_events ||
st.unmute_events || audio_trace_events_total())
fail("reset events");
print_stats("reset");
return 0;
}
"""
def default_cc() -> str:
if DEFAULT_GCC.exists():
return str(DEFAULT_GCC).replace("\\", "/")
for name in ("cc", "gcc", "clang"):
path = shutil.which(name)
if path:
return path
return str(DEFAULT_GCC).replace("\\", "/")
def normalize_cc(cc: str) -> str:
if "\\" in cc and len(cc) >= 3 and cc[1] == ":":
return cc.replace("\\", "/")
return cc
def build(cc: str, work: pathlib.Path, production: bool) -> pathlib.Path:
source = work / "audio_trace_guard_driver.c"
source.write_text(DRIVER_C, encoding="utf-8")
exe = work / ("audio_trace_prod.exe" if production else "audio_trace_debug.exe")
cmd = [
cc,
"-std=c11",
"-O2",
"-fno-lto",
"-I",
str(ROOT / "runtime" / "include"),
]
if production:
cmd.append("-DPSX_NO_DEBUG_TOOLS=1")
cmd.extend([
str(source),
str(ROOT / "runtime" / "src" / "audio_trace.c"),
"-o",
str(exe),
])
subprocess.run(cmd, cwd=work, check=True)
return exe
def run(exe: pathlib.Path, path: pathlib.Path, mode: str) -> str:
return subprocess.run(
[str(exe), str(path), mode],
cwd=exe.parent,
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.strip()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--cc", default=default_cc())
args = parser.parse_args()
cc = normalize_cc(args.cc)
with tempfile.TemporaryDirectory(prefix="psx-audio-trace-") as td:
work = pathlib.Path(td)
debug = build(cc, work, False)
prod = build(cc, work, True)
debug_out = run(debug, work / "debug.wav", "debug")
prod_out = run(prod, work / "prod.wav", "production")
if debug_out != prod_out:
raise AssertionError(f"stats/event output changed:\n{debug_out}\n{prod_out}")
print(debug_out)
print("PASS: audio trace PCM history is debug-tool only; stats/events match")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except AssertionError as exc:
print(f"FAIL: {exc}")
sys.exit(1)
except subprocess.CalledProcessError as exc:
print(f"FAIL: command exited {exc.returncode}: {' '.join(map(str, exc.cmd))}")
sys.exit(1)
+494
View File
@@ -0,0 +1,494 @@
#!/usr/bin/env python3
"""Validate production GP0 history gating against actual gpu.c.
This is a parser/register/VRAM regression with renderer stubs, not a full
backend rendering-correctness test.
"""
from __future__ import annotations
import argparse
import pathlib
import platform
import shutil
import subprocess
import sys
import tempfile
ROOT = pathlib.Path(__file__).resolve().parents[2]
GPU_C = ROOT / "runtime" / "src" / "gpu.c"
DEFAULT_GCC = pathlib.Path(r"C:\msys64\mingw64\bin\gcc.exe")
DRIVER_C = r"""
#include "gpu.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
extern uint64_t s_frame_count;
uint32_t gpu_get_opcode_count(uint8_t op);
uint64_t gpu_get_gp0_count(void);
void gpu_get_gp0_stats(uint64_t* nop, uint64_t* fill, uint64_t* draw,
uint64_t* env, uint64_t* copy);
uint32_t gpu_snapshot_bytes(void);
void gpu_snapshot_write(uint8_t *p);
static uint32_t timeline_hash = 2166136261u;
static int checkpoint_failed = 0;
static FILE *trace_file;
static uint32_t mix(uint32_t h, uint32_t v) {
return (h ^ v) * 16777619u;
}
static void checkpoint(void) {
uint32_t n = gpu_snapshot_bytes();
uint8_t *snap = (uint8_t *)malloc(n ? n : 1u);
if (!snap) {
checkpoint_failed = 1;
return;
}
gpu_snapshot_write(snap);
if (fwrite(&n, sizeof(n), 1, trace_file) != 1 ||
fwrite(snap, 1, n, trace_file) != n) checkpoint_failed = 1;
timeline_hash = mix(timeline_hash, n);
for (uint32_t i = 0; i < n; i++) timeline_hash = mix(timeline_hash, snap[i]);
free(snap);
for (int y = 0; y < 512; y++) {
uint16_t row[1024];
for (int x = 0; x < 1024; x++) {
row[x] = gpu_vram_peek(x, y);
timeline_hash = mix(timeline_hash, row[x]);
}
if (fwrite(row, sizeof(row), 1, trace_file) != 1) checkpoint_failed = 1;
}
}
static void emit_env(uint32_t word) {
gpu_write_gp0(word);
s_frame_count++;
checkpoint();
}
static void emit_fill(uint32_t color, uint32_t xy, uint32_t wh) {
gpu_write_gp0(0x02000000u | color);
gpu_write_gp0(xy);
gpu_write_gp0(wh);
s_frame_count++;
checkpoint();
}
static void emit_upload_2x1(uint32_t xy, uint32_t pixels) {
gpu_write_gp0(0xA0000000u);
gpu_write_gp0(xy);
gpu_write_gp0((1u << 16) | 2u);
gpu_write_gp0(pixels);
s_frame_count++;
checkpoint();
}
static void emit_copy(uint32_t src_xy, uint32_t dst_xy, uint32_t wh) {
gpu_write_gp0(0x80000000u);
gpu_write_gp0(src_xy);
gpu_write_gp0(dst_xy);
gpu_write_gp0(wh);
s_frame_count++;
checkpoint();
}
static void emit_polyline(void) {
gpu_write_gp0(0x4800007Fu);
gpu_write_gp0((20u << 16) | 10u);
gpu_write_gp0(0x55555555u);
s_frame_count++;
checkpoint();
}
static uint32_t hash_state(void) {
GpuDrawArea a;
uint64_t nop = 0, fill = 0, draw = 0, env = 0, copy = 0;
gpu_get_draw_area(&a);
gpu_get_gp0_stats(&nop, &fill, &draw, &env, &copy);
uint32_t h = 2166136261u;
#define MIX(v) do { h = (h ^ (uint32_t)(v)) * 16777619u; } while (0)
MIX(gpu_get_gp0_count());
MIX(nop); MIX(fill); MIX(draw); MIX(env); MIX(copy);
MIX(gpu_get_opcode_count(0x00)); MIX(gpu_get_opcode_count(0x02));
MIX(gpu_get_opcode_count(0xA0)); MIX(gpu_get_opcode_count(0xE1));
MIX(gpu_get_opcode_count(0xE3)); MIX(gpu_get_opcode_count(0xE4));
MIX(gpu_get_opcode_count(0xE5)); MIX(gpu_get_opcode_count(0xE6));
MIX(a.left); MIX(a.top); MIX(a.right); MIX(a.bottom);
MIX((uint32_t)a.offset_x); MIX((uint32_t)a.offset_y);
MIX(gpu_vram_peek(16, 5)); MIX(gpu_vram_peek(31, 5));
MIX(gpu_vram_peek(40, 9)); MIX(gpu_vram_peek(41, 9));
MIX(gpu_read_gpustat());
MIX(gpu_ws_census_seq());
MIX(timeline_hash);
return h;
#undef MIX
}
int main(int argc, char **argv) {
GpuGp0RingEntry entries[16], copy_entries[16], poly_entries[16];
uint32_t oldest = 99, newest = 99;
if (argc != 2 || !(trace_file = fopen(argv[1], "wb"))) return 2;
gpu_init();
gpu_ws_census_set(0);
checkpoint();
gpu_set_gp0_source(0x00123400u);
emit_env(0xE10003FFu);
gpu_set_gp0_source(0x00123404u);
emit_env(0xE3000000u | (3u << 10) | 2u);
gpu_set_gp0_source(0x00123408u);
emit_env(0xE4000000u | (80u << 10) | 90u);
gpu_set_gp0_source(0x0012340Cu);
emit_env(0xE5000000u | (8u << 11) | 7u);
gpu_set_gp0_source(0x00123410u);
emit_env(0xE6000001u);
gpu_set_gp0_source(0x00123414u);
emit_fill(0x0000FFu, (5u << 16) | 16u, (1u << 16) | 16u);
gpu_set_gp0_source(0x00123418u);
emit_upload_2x1((9u << 16) | 40u, 0x9ABC1234u);
gpu_set_gp0_source(0x0012341Cu);
emit_copy((9u << 16) | 40u, (10u << 16) | 42u, (1u << 16) | 2u);
gpu_set_gp0_source(0x00123420u);
emit_polyline();
gpu_set_gp0_source(0x00123424u);
emit_env(0x00000000u);
if (fclose(trace_file) != 0) checkpoint_failed = 1;
if (checkpoint_failed) {
fprintf(stderr, "checkpoint allocation/write failed\n");
return 2;
}
gpu_gp0_ring_frame_span(&oldest, &newest);
int dumped = gpu_gp0_ring_dump_frame(0, entries, 16);
int copy_dumped = gpu_gp0_ring_dump_frame(7, copy_entries, 16);
int poly_dumped = gpu_gp0_ring_dump_frame(8, poly_entries, 16);
printf("hash=%08x total=%llu cap=%u max=%u span=%u:%u dump0=%d "
"src=%08x op=%02x cmd0=%08x "
"copydump=%d copysrc=%08x copyop=%02x copycsp=%08x "
"polydump=%d polyop=%02x polycmd0=%08x\n",
hash_state(), (unsigned long long)gpu_gp0_ring_total(),
gpu_gp0_ring_capacity(), gpu_gp0_ring_max_words(), oldest, newest,
dumped, dumped > 0 ? entries[0].src_addr : 0u,
dumped > 0 ? entries[0].opcode : 0u,
dumped > 0 ? entries[0].cmd[0] : 0u,
copy_dumped, copy_dumped > 0 ? copy_entries[0].src_addr : 0u,
copy_dumped > 0 ? copy_entries[0].opcode : 0u,
copy_dumped > 0 ? copy_entries[0].csp : 0u,
poly_dumped, poly_dumped > 0 ? poly_entries[0].opcode : 0u,
poly_dumped > 0 ? poly_entries[0].cmd[0] : 0u);
return 0;
}
"""
STUBS_C = r"""
#include "gpu.h"
#include "gpu_render.h"
#include "cpu_state.h"
#include "color_lut.h"
#include "ws_aspect_cone_math.h"
#include "ws_ui_group.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
uint64_t s_frame_count = 0;
uint32_t g_debug_last_store_pc = 0x80012340u;
uint32_t g_debug_current_func_addr = 0x80045670u;
CPUState *debug_cpu_ptr = 0;
uint32_t i_stat = 0;
uint32_t i_mask = 0;
static uint16_t *g_vram;
uint32_t debug_guest_ra(void) { return 0x8000AAAAu; }
uint32_t debug_guest_sp(void) { return 0x8010FFF0u; }
uint8_t *memory_get_ram_ptr(void) { static uint8_t ram[2 * 1024 * 1024]; return ram; }
uint16_t psx_read_half(uint32_t addr) { (void)addr; return 0; }
uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; }
uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; }
void psx_fatal_halt(const char *reason) { fprintf(stderr, "%s\n", reason); abort(); }
void crash_trace_note_gpu_fatal(uint32_t opcode, uint32_t word) { (void)opcode; (void)word; }
void event_ring_note_gpu_irq(uint32_t stat) { (void)stat; }
int g_psx_vram_dirty_tracking = 0;
int g_exec_phase = 0;
void gpu_vram_dirty_mark_all(void) {}
void gpu_vram_dirty_mark_row_impl(uint32_t row) { (void)row; }
void gpu_vram_dirty_mark_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h)
{ (void)x; (void)y; (void)w; (void)h; }
void text_xlate_vram_upload(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{ (void)x; (void)y; (void)w; (void)h; }
int ws_cull_should_keep(uint32_t addr) { (void)addr; return 1; }
int ws_ui_group_should_keep(uint32_t addr) { (void)addr; return 1; }
uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { return address; }
void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
int32_t display_width, int dense_menu)
{ (void)items; (void)count; (void)display_width; (void)dense_menu; }
int32_t ws_ui_anchor_for_bounds(int32_t x, int32_t width, int32_t display_width)
{ (void)x; (void)width; return display_width / 2; }
int gte_geometry_correction_enabled(void) { return 0; }
void pgxp_set_enabled(int enabled) { (void)enabled; }
int pgxp_get_gte_sxy_checked(uint32_t slot, uint32_t packed, int require_valid,
int32_t *x16, int32_t *y16)
{ (void)slot; (void)packed; (void)require_valid; (void)x16; (void)y16; return 0; }
int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
int32_t int_x, int32_t int_y,
int32_t *x16, int32_t *y16, uint16_t *sz)
{ (void)addr; (void)packet_word; (void)int_x; (void)int_y; (void)x16; (void)y16; (void)sz; return 0; }
int gte_precision_load_word(uint32_t addr, uint32_t packed,
int32_t *x16, int32_t *y16, uint16_t *z)
{ (void)addr; (void)packed; (void)x16; (void)y16; (void)z; return 0; }
void pgxp_invalidate_all(void) {}
void pgxp_invalidate_word(uint32_t addr) { (void)addr; }
void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; }
void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux)
{ (void)kind; (void)detail; (void)aux; }
int psx_get_in_exception(void) { return 0; }
int psx_netplay_active(void) { return 0; }
int sio_hold_present_for_card(void) { return 0; }
uint32_t psx_compiled_irq_resume_pc = 0;
uint32_t psx_last_irq_check_pc = 0;
uint32_t psx_netplay_rb_sticky_bb_pc = 0;
void mod_runtime_on_vblank(void) {}
void sio_ape_card_unstick_pump(void) {}
void psx_write_half(uint32_t addr, uint16_t val) { (void)addr; (void)val; }
int mdec_recently_active(uint32_t within_frames) { (void)within_frames; return 0; }
uint32_t psx_ws_widen_angle_q12(uint32_t vanilla, int extent_pixels)
{ (void)extent_pixels; return vanilla; }
int psx_ws_aspect_cone_contains(int32_t x, int32_t z, int32_t y,
int32_t fx, int32_t fz, int32_t fy,
uint32_t threshold, int extent_pixels)
{ (void)x; (void)z; (void)y; (void)fx; (void)fz; (void)fy; (void)threshold; (void)extent_pixels; return 0; }
uint32_t sw_perspective_triangle_count(void) { return 0; }
bool screen_kind_from_name(const char* name, ScreenKind* out)
{ (void)name; if (out) *out = SCREEN_RAW; return 0; }
ColorLut* color_lut_create(const ColorSettings* settings)
{ (void)settings; return 0; }
void color_lut_destroy(ColorLut* lut) { (void)lut; }
bool color_lut_is_passthrough(const ColorLut* lut) { (void)lut; return 1; }
void color_lut_map555(const ColorLut* lut, uint16_t bgr555,
uint8_t* r, uint8_t* g, uint8_t* b)
{ (void)lut; if (r) *r = (uint8_t)(bgr555 & 0x1F); if (g) *g = (uint8_t)((bgr555 >> 5) & 0x1F); if (b) *b = (uint8_t)((bgr555 >> 10) & 0x1F); }
int ws_sprt_fixed_transform(int32_t *x, int32_t y, int w)
{ (void)x; (void)y; return w; }
int ws_nw_hud_shift(int32_t x, int w) { (void)x; (void)w; return 0; }
int ws_is_fb_base(int x) { (void)x; return 0; }
int ws_local_viewport_draw_target(int *base) { if (base) *base = 0; return 0; }
void ws_nw_sync_target(void) {}
int ws_active(void) { return 0; }
int ws_engaged(void) { return 0; }
void gr_init(uint16_t *vram) { g_vram = vram; }
uint16_t gr_vram_read(int x, int y) { return g_vram[(y & 511) * 1024 + (x & 1023)]; }
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *pixels)
{
for (int yy = 0; yy < h; yy++)
for (int xx = 0; xx < w; xx++)
g_vram[((y + yy) & 511) * 1024 + ((x + xx) & 1023)] = pixels[yy * w + xx];
}
void gr_fill_rect(int x, int y, int w, int h, uint16_t color)
{
for (int yy = 0; yy < h; yy++)
for (int xx = 0; xx < w; xx++)
g_vram[((y + yy) & 511) * 1024 + ((x + xx) & 1023)] = color;
}
void gr_wide_clear(int x, int y, int h, uint16_t color)
{ (void)x; (void)y; (void)h; (void)color; }
void gr_wide_disable_target(void) {}
void gr_wide_configure(int width, int offset) { (void)width; (void)offset; }
void gr_wide_set_target(int base) { (void)base; }
void gr_wide_clear_margins(int base, int y, int h, uint16_t color, int mode)
{ (void)base; (void)y; (void)h; (void)color; (void)mode; }
void gr_set_draw_area(int l, int t, int r, int b) { (void)l; (void)t; (void)r; (void)b; }
void gr_set_draw_offset(int x, int y) { (void)x; (void)y; }
void gr_set_mask_bits(int set, int check) { (void)set; (void)check; }
void gr_set_texture_window(uint32_t value) { (void)value; }
void gr_set_semi_transparency(int enabled, int mode) { (void)enabled; (void)mode; }
void gr_set_color_modulation(int r, int g, int b, int raw)
{ (void)r; (void)g; (void)b; (void)raw; }
void gr_set_perspective_triangle(int enabled, float a, float b, float c)
{ (void)enabled; (void)a; (void)b; (void)c; }
void gr_set_precise_triangle(int enabled, int32_t ax, int32_t ay, int32_t bx,
int32_t by, int32_t cx, int32_t cy)
{ (void)enabled; (void)ax; (void)ay; (void)bx; (void)by; (void)cx; (void)cy; }
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h)
{ (void)sx; (void)sy; (void)dx; (void)dy; (void)w; (void)h; }
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t color)
{ (void)x; (void)y; (void)w; (void)h; (void)color; }
void gr_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t c)
{ (void)x0;(void)y0;(void)x1;(void)y1;(void)x2;(void)y2;(void)c; }
void gr_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2)
{ (void)x0;(void)y0;(void)c0;(void)x1;(void)y1;(void)c1;(void)x2;(void)y2;(void)c2; }
void gr_draw_line(int x0,int y0,int x1,int y1,uint16_t c)
{ (void)x0;(void)y0;(void)x1;(void)y1;(void)c; }
void gr_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1)
{ (void)x0;(void)y0;(void)c0;(void)x1;(void)y1;(void)c1; }
void gr_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t clut_x,uint16_t clut_y,uint16_t texpage)
{ (void)x;(void)y;(void)w;(void)h;(void)u;(void)v;(void)clut_x;(void)clut_y;(void)texpage; }
void gr_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t clut_x,uint16_t clut_y,uint16_t texpage)
{ (void)x;(void)y;(void)w;(void)h;(void)u0;(void)v0;(void)u1;(void)v1;(void)clut_x;(void)clut_y;(void)texpage; }
void gr_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t clut_x,uint16_t clut_y,uint16_t texpage)
{ (void)x0;(void)y0;(void)u0;(void)v0;(void)x1;(void)y1;(void)u1;(void)v1;(void)x2;(void)y2;(void)u2;(void)v2;(void)clut_x;(void)clut_y;(void)texpage; }
void gr_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t clut_x,uint16_t clut_y,uint16_t texpage,int raw)
{ (void)x0;(void)y0;(void)u0;(void)v0;(void)c0;(void)x1;(void)y1;(void)u1;(void)v1;(void)c1;(void)x2;(void)y2;(void)u2;(void)v2;(void)c2;(void)clut_x;(void)clut_y;(void)texpage;(void)raw; }
"""
def fail(message: str) -> None:
raise AssertionError(message)
def default_cc() -> str:
if DEFAULT_GCC.exists():
return str(DEFAULT_GCC).replace("\\", "/")
for name in ("cc", "gcc", "clang"):
path = shutil.which(name)
if path:
return path
return str(DEFAULT_GCC)
def function_body(source: str, signature: str) -> str:
start = source.index(signature)
brace = source.index("{", start)
depth = 0
for pos in range(brace, len(source)):
if source[pos] == "{":
depth += 1
elif source[pos] == "}":
depth -= 1
if depth == 0:
return source[brace : pos + 1]
raise AssertionError(f"unterminated function: {signature}")
def assert_source_shape(gpu: str) -> None:
guard = gpu.index("#ifndef PSX_NO_DEBUG_TOOLS", gpu.index("debug tools only"))
alt = gpu.index("#else", guard)
end = gpu.index("#endif", alt)
debug_block = gpu[guard:alt]
prod_block = gpu[alt:end]
for token in ("GP0_RING_CAP", "static GpuGp0RingEntry *gp0_ring",
"gp0_capture_builder_chain", "g_gp0_last_copy_sp",
"debug_guest_ra()", "debug_guest_sp()"):
if token not in debug_block:
fail(f"debug GP0 history block lost {token}")
if token in prod_block:
fail(f"production GP0 history block still references {token}")
for token in ("gpu_gp0_ring_total(void) { return 0; }",
"gpu_gp0_ring_capacity(void) { return 0; }",
"return GPU_GP0_RING_MAX_WORDS;",
"return 0;"):
if token not in prod_block:
fail(f"production unavailable accessor missing {token}")
set_source = function_body(gpu, "void gpu_set_gp0_source(")
if "gp0_next_source_addr = addr;" not in set_source:
fail("gpu_set_gp0_source no longer preserves source tracking")
execute = function_body(gpu, "static void gp0_execute_command(")
order = [execute.index(token) for token in (
"gp0_opcode_count[opcode]++",
"gp0_ring_record(gp0_cmd_buf, gp0_words_needed)",
"ws_bg_phase_note(opcode)",
"ws_census_record(opcode, cvx, cvy)",
"ws_note_overhang(opcode)",
)]
if order != sorted(order):
fail("GP0 command accounting/ring/WS ordering changed")
if "gp0_ring_record(hdr_only, 1);" not in gpu:
fail("polyline header capture hook was removed")
def write_sources(work: pathlib.Path) -> None:
(work / "driver.c").write_text(DRIVER_C, encoding="utf-8")
(work / "stubs.c").write_text(STUBS_C, encoding="utf-8")
def build(cc: str, work: pathlib.Path, name: str, prod: bool) -> pathlib.Path:
exe = work / f"{name}.exe"
cmd = [
cc, "-std=c11", "-O2", "-ffunction-sections", "-fdata-sections",
"-I", str(ROOT / "runtime" / "include"),
]
if prod:
cmd.append("-DPSX_NO_DEBUG_TOOLS")
cmd.extend([
str(work / "driver.c"),
str(GPU_C),
str(work / "stubs.c"),
])
if platform.system() == "Darwin":
cmd.append("-Wl,-dead_strip")
else:
cmd.append("-Wl,--gc-sections")
cmd.extend(["-o", str(exe)])
subprocess.run(cmd, cwd=work, check=True)
return exe
def run(exe: pathlib.Path) -> dict[str, str]:
proc = subprocess.run([str(exe), str(exe.with_suffix(".state"))],
cwd=exe.parent, check=True, text=True,
stdout=subprocess.PIPE)
return dict(part.split("=", 1) for part in proc.stdout.strip().split())
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--cc", default=default_cc())
args = parser.parse_args()
cc = args.cc.replace("\\", "/")
assert_source_shape(GPU_C.read_text(encoding="utf-8"))
with tempfile.TemporaryDirectory(prefix="psx-gp0-history-") as td:
work = pathlib.Path(td)
write_sources(work)
debug = build(cc, work, "debug", prod=False)
prod = build(cc, work, "prod", prod=True)
debug_out = run(debug)
prod_out = run(prod)
debug_state = debug.with_suffix(".state").read_bytes()
prod_state = prod.with_suffix(".state").read_bytes()
if not debug_state or debug_state != prod_state:
fail("production gate changed checkpoint GPU snapshot/VRAM bytes")
if debug_out["hash"] != prod_out["hash"]:
fail("production GP0 history gate changed guest-visible GPU state")
if prod_out["total"] != "0" or prod_out["cap"] != "0":
fail("production GP0 history remains available")
if prod_out["span"] != "0:0" or prod_out["dump0"] != "0":
fail("production GP0 history accessors should report empty capture")
if prod_out["copydump"] != "0" or prod_out["polydump"] != "0":
fail("production GP0 history frame dumps should be empty")
if debug_out["total"] != "10" or int(debug_out["cap"]) == 0:
fail("debug GP0 history stopped recording")
if debug_out["src"] != "00123400" or debug_out["op"] != "e1":
fail("debug GP0 history lost source/opcode tracking")
if debug_out["cmd0"] != "e10003ff":
fail("debug GP0 history lost command-word capture")
if debug_out["copydump"] != "1" or debug_out["copysrc"] != "0012341c":
fail("debug GP0 history lost copy source tracking")
if debug_out["copyop"] != "80" or debug_out["copycsp"] != "8010fff0":
fail("debug GP0 history lost copy builder diagnostics")
if debug_out["polydump"] != "1" or debug_out["polyop"] != "48":
fail("debug GP0 history lost polyline header capture")
if debug_out["polycmd0"] != "4800007f":
fail("debug GP0 history lost polyline command word")
print("PASS: actual gpu.c production GP0 history gate preserves GPU state")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except AssertionError as exc:
print(f"FAIL: {exc}")
sys.exit(1)
except subprocess.CalledProcessError as exc:
print(f"FAIL: command exited {exc.returncode}: {' '.join(map(str, exc.cmd))}")
sys.exit(1)