Avoid inactive GPU and BIOS diagnostic recording

This commit is contained in:
Matthew Stanley
2026-10-24 19:07:36 -07:00
parent e7ed72736a
commit cecf4c5d9c
8 changed files with 147 additions and 11 deletions
+11 -1
View File
@@ -263,6 +263,12 @@ if(BUILD_TESTING)
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
# fixture_link uses GNU nm/linker diagnostics for its unrelated seams.
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
add_test(NAME bios_hle_recording_test COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_bios_hle_recording.py"
--cc "${CMAKE_C_COMPILER}")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_test(NAME menu_video_snapshot_test COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_menu_video_snapshot.py
@@ -1131,8 +1137,10 @@ if(BUILD_TESTING)
src/ws_ui_group.c
src/psx_video_timing.c
src/gpu_timeline.c)
add_executable(gpu_census_test tests/test_gpu_census.c
src/psx_video_timing.c src/gpu_timeline.c)
foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu native_wide_projection
gpu_render_pass_provenance)
gpu_render_pass_provenance gpu_census)
target_include_directories(${_anchor_test}_test PRIVATE include src)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${_anchor_test}_test PRIVATE -ffunction-sections -fdata-sections)
@@ -1148,6 +1156,8 @@ if(BUILD_TESTING)
add_test(NAME ws_view_anchor_gpu_test COMMAND ws_view_anchor_gpu_test)
add_test(NAME native_wide_projection_test COMMAND native_wide_projection_test)
add_test(NAME gpu_render_pass_provenance_test COMMAND gpu_render_pass_provenance_test)
add_test(NAME gpu_census_test COMMAND gpu_census_test
"${CMAKE_CURRENT_BINARY_DIR}/gpu-census-test.csv")
add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c)
target_include_directories(ws_repeat_rect_test PRIVATE include)
+4 -2
View File
@@ -110,8 +110,10 @@ typedef struct PsxHleCallEntry {
#define PSX_HLE_RING_CAP 16384 /* power of two */
/* Snapshot accessors: seq is the total number of records ever written; entry
* i (seq-CAP .. seq-1 valid window) returns the ring slot for that sequence. */
/* Debug builds only; PSX_NO_DEBUG_TOOLS returns an empty sequence/NULL and
* performs no diagnostic guest reads. Snapshot accessors: seq is the total
* number of records ever written; entry i (seq-CAP .. seq-1 valid window)
* returns the ring slot for that sequence. */
uint64_t psx_hle_ring_seq(void);
const PsxHleCallEntry* psx_hle_ring_entry(uint64_t seq_index);
+2 -1
View File
@@ -739,7 +739,8 @@ void gpu_ws_set_margin_override(int v);
* through the tagged sprite funnel). */
void gpu_ws_set_nw_hud_tag_rects(int on);
/* Always-on draw-census ring: every drawn primitive records frame / source
/* Explicit diagnostic draw-census ring: arm before the capture window.
* Every drawn primitive records frame / source
* addr / camera / first-vertex screen pos, so object spawn/despawn and edge
* culls are observable in data. Dump frames [f0,f1] to a CSV file. */
int gpu_ws_census_dump(uint32_t f0, uint32_t f1, const char *path);
+17 -1
View File
@@ -87,8 +87,9 @@ static int s_shell_skipped = 0; /* boot skip already fired */
* delivery gives the callback, reused here so the dispatch return contract
* matches. 0 = unknown for this BIOS: callback delivery stays LLE. */
/* ── always-on ring ──────────────────────────────────────────────────────── */
/* ── debug call-history ring ─────────────────────────────────────────────── */
#ifndef PSX_NO_DEBUG_TOOLS
static PsxHleCallEntry s_ring[PSX_HLE_RING_CAP];
static uint64_t s_ring_seq = 0;
@@ -153,6 +154,21 @@ const PsxHleCallEntry* psx_hle_ring_entry(uint64_t seq_index)
if (s_ring_seq - seq_index > PSX_HLE_RING_CAP) return NULL; /* evicted */
return &s_ring[seq_index & (PSX_HLE_RING_CAP - 1)];
}
#else
/* The debug dump is the ring's only consumer. Keep functional BIOS handling
* independent of forensic reads, repeat scans and storage in production. */
static inline void hle_record(uint32_t vector, uint32_t fn, const CPUState* cpu,
uint32_t v0, uint8_t route)
{
(void)vector; (void)fn; (void)cpu; (void)v0; (void)route;
}
uint64_t psx_hle_ring_seq(void) { return 0; }
const PsxHleCallEntry* psx_hle_ring_entry(uint64_t seq_index)
{
(void)seq_index;
return NULL;
}
#endif
/* ── B0 event services (SCPH1001 kernel semantics, on real guest EvCBs) ──── */
+9 -6
View File
@@ -7299,7 +7299,7 @@ void gpu_set_gp0_source(uint32_t addr) {
gp0_next_source_addr = addr;
}
/* ---- Draw census ring (ALWAYS-ON) -----------------------------------------
/* ---- Draw census ring (explicit diagnostic capture) -----------------------
* Every drawn primitive (GP0 opcode 0x20-0x7F) records: frame, the prim's DMA
* source address, the live camera (scratchpad camX/camY), and the prim's first
* vertex in DRAWING space (pre draw_offset). Purpose: see object spawn/despawn
@@ -7308,7 +7308,8 @@ void gpu_set_gp0_source(uint32_t addr) {
* last recorded screen-x shows how far on-screen it still was — i.e. the
* effective (4:3-sized) despawn margin, which the 16:9 view exceeds. A prim
* present in the census but absent on screen would instead indict the renderer
* gate. Query via TCP `ws_census` → CSV file (large dumps mustn't ride TCP). */
* gate. Arm via TCP `ws_census action=on`, then dump to CSV (large dumps
* mustn't ride TCP). No gameplay consumer depends on this forensic ring. */
#define WS_CENSUS_CAP (1u << 21) /* 2,097,152 entries * 24B = 48 MiB */
typedef struct {
uint32_t frame;
@@ -7322,7 +7323,7 @@ typedef struct {
} WsCensusEntry;
static WsCensusEntry *ws_census = NULL;
static uint64_t ws_census_seq = 0;
static int ws_census_on = 1; /* always-on; toggle via ws_census */
static int ws_census_on = 0; /* arm only for a diagnostic window */
extern uint16_t psx_read_half(uint32_t addr);
/* Screen-X extent (raw vertex coords = SX, pre-draw_offset) across a prim's
@@ -7496,9 +7497,11 @@ static void gp0_execute_command(void) {
/* Draw-census: capture every drawing primitive's first vertex + camera. */
if (opcode >= 0x20 && opcode <= 0x7F) {
int32_t cvx, cvy;
parse_vertex(gp0_cmd_buf[1], &cvx, &cvy);
ws_census_record(opcode, cvx, cvy);
if (ws_census_on) {
int32_t cvx, cvy;
parse_vertex(gp0_cmd_buf[1], &cvx, &cvy);
ws_census_record(opcode, cvx, cvy);
}
ws_note_overhang(opcode); /* 2D-only-scene classifier world signal */
ws_note_backdrop(opcode); /* ...and its finite-backdrop veto */
}
+44
View File
@@ -0,0 +1,44 @@
#ifdef NDEBUG
#undef NDEBUG
#endif
#include "bios_hle.h"
#include "cpu_state.h"
#include "psx_bios_image.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
PsxBiosImageInfo psx_bios_image = {.shell_entry_phys=0x30000u};
uint64_t psx_cycle_count=100;
uint64_t psx_get_cycle_count(void) { return psx_cycle_count; }
static unsigned reads;
static uint32_t read_word(uint32_t address) {
assert(address==0xA0000108u);
++reads;
return 0; /* no running PCB: only the first forensic attribution read */
}
int psx_get_in_exception(void) { return 0; }
int fntrace_is_game_started(void) { return 1; }
int main(void) {
CPUState cpu={0};
cpu.read_word=read_word;
cpu.gpr[9]=0x17u; cpu.gpr[31]=0x80012340u;
CPUState before=cpu;
psx_bios_hle_configure(0,1);
assert(g_psx_bios_hle_hook);
assert(!g_psx_bios_hle_hook(&cpu,0xA0u));
assert(!g_psx_bios_hle_hook(&cpu,0xA0u));
assert(!memcmp(&before,&cpu,sizeof(cpu)) && psx_cycle_count==100);
#if EXPECT_RECORDING
assert(reads==2 && psx_hle_ring_seq()==1);
const PsxHleCallEntry *entry=psx_hle_ring_entry(0);
assert(entry && entry->repeat==2 && entry->route==PSX_HLE_ROUTE_LLE);
#else
assert(!reads && !psx_hle_ring_seq() && !psx_hle_ring_entry(0));
#endif
psx_bios_hle_configure(0,0);
assert(!g_psx_bios_hle_hook);
puts("PASS BIOS recording: caller state/time preserved; diagnostic reads only in debug builds");
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
"""Actual BIOS dispatch keeps diagnostic guest reads out of production."""
import argparse
from pathlib import Path
import tempfile
from fixture_link import build_and_run
if __name__=='__main__':
parser=argparse.ArgumentParser();parser.add_argument('--cc',default='gcc')
args=parser.parse_args();here=Path(__file__).resolve().parent
for recording in (0,1):
with tempfile.TemporaryDirectory(prefix='bios-recording-') as directory:
work=Path(directory)
(work/'fixture.c').write_text(f'#define EXPECT_RECORDING {recording}\n'+
(here/'test_bios_hle_recording.c').read_text())
build_and_run(args.cc,work,here.parent,'-O2',work,['bios_hle.c'],'fixture.c',
defines=[] if recording else ['PSX_NO_DEBUG_TOOLS=1'])
print('PASS: production dispatch records no forensic guest reads; debug repeats remain available')
+43
View File
@@ -0,0 +1,43 @@
/* Exercise default-off and explicitly armed capture through real GP0 dispatch. */
#define main gpu_dot_fixture_main
#include "test_gpu_textured_dot_nw_shift_exec.c"
#undef main
static void draw_dot(void) {
set_dot_packet(0x10004u, 12, 34);
gp0_execute_command();
}
int main(int argc, char **argv) {
assert(argc == 2);
reset_gpu_state_for_test();
draw_dot();
assert(last_textured_rect.calls == 1);
assert(last_textured_rect.x == 12 && last_textured_rect.y == 34);
assert(gpu_ws_census_seq() == 0 && ws_census == NULL);
gpu_ws_census_set(1);
draw_dot();
assert(last_textured_rect.calls == 2 && gpu_ws_census_seq() == 1);
assert(gpu_ws_census_dump(100, 100, argv[1]) == 1);
FILE *csv = fopen(argv[1], "r");
assert(csv);
char line[256];
assert(fgets(line, sizeof(line), csv));
assert(strncmp(line, "frame,src_addr,", 15) == 0);
assert(fgets(line, sizeof(line), csv));
assert(strncmp(line, "100,0x00010004,", 15) == 0);
assert(strstr(line, ",12,34,12,12,0,0x6C,0"));
assert(fgets(line, sizeof(line), csv) == NULL);
assert(fclose(csv) == 0);
assert(remove(argv[1]) == 0);
gpu_ws_census_set(0);
draw_dot();
assert(last_textured_rect.calls == 3 && gpu_ws_census_seq() == 1);
free(ws_census);
ws_census = NULL;
puts("PASS: drawing works without census allocation; armed capture exports and stops");
return 0;
}