diff --git a/docs/performance/EXPERIMENTS.md b/docs/performance/EXPERIMENTS.md index fba1becf..4d394d65 100644 --- a/docs/performance/EXPERIMENTS.md +++ b/docs/performance/EXPERIMENTS.md @@ -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. diff --git a/docs/performance/RUNTIME_AUDIT.md b/docs/performance/RUNTIME_AUDIT.md index 054feeb3..cdb8b4e2 100644 --- a/docs/performance/RUNTIME_AUDIT.md +++ b/docs/performance/RUNTIME_AUDIT.md @@ -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. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 3d1f2e2a..ddc4fa37 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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 diff --git a/runtime/include/audio_trace.h b/runtime/include/audio_trace.h index 6d55b102..544faaed 100644 --- a/runtime/include/audio_trace.h +++ b/runtime/include/audio_trace.h @@ -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); diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index c831f207..2660cdff 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -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; diff --git a/runtime/src/audio_trace.c b/runtime/src/audio_trace.c index 9008c094..7c312bd8 100644 --- a/runtime/src/audio_trace.c +++ b/runtime/src/audio_trace.c @@ -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 /* 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 diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 8d91f79a..b5b818a9 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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 diff --git a/runtime/tests/test_audio_trace_pcm_guard.py b/runtime/tests/test_audio_trace_pcm_guard.py new file mode 100644 index 00000000..870092a1 --- /dev/null +++ b/runtime/tests/test_audio_trace_pcm_guard.py @@ -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 +#include +#include +#include + +#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) diff --git a/runtime/tests/test_gpu_gp0_history_guard.py b/runtime/tests/test_gpu_gp0_history_guard.py new file mode 100644 index 00000000..9c8ecc56 --- /dev/null +++ b/runtime/tests/test_gpu_gp0_history_guard.py @@ -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 +#include +#include + +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, ©); + 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 +#include +#include +#include +#include + +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)