mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
WIP: stage production history gates pending quiet-host timing
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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, ©);
|
||||
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)
|
||||
Reference in New Issue
Block a user