diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 6e788912..bd1a9c21 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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) diff --git a/runtime/include/bios_hle.h b/runtime/include/bios_hle.h index f9547eb8..93dae2af 100644 --- a/runtime/include/bios_hle.h +++ b/runtime/include/bios_hle.h @@ -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); diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 8a70965c..ffd02501 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -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); diff --git a/runtime/src/bios_hle.c b/runtime/src/bios_hle.c index f44808e8..7745d381 100644 --- a/runtime/src/bios_hle.c +++ b/runtime/src/bios_hle.c @@ -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) ──── */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 66fc8b68..a8065285 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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 */ } diff --git a/runtime/tests/test_bios_hle_recording.c b/runtime/tests/test_bios_hle_recording.c new file mode 100644 index 00000000..d71f7a7b --- /dev/null +++ b/runtime/tests/test_bios_hle_recording.c @@ -0,0 +1,44 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include "bios_hle.h" +#include "cpu_state.h" +#include "psx_bios_image.h" +#include +#include +#include + +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; +} diff --git a/runtime/tests/test_bios_hle_recording.py b/runtime/tests/test_bios_hle_recording.py new file mode 100644 index 00000000..a02996cc --- /dev/null +++ b/runtime/tests/test_bios_hle_recording.py @@ -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') diff --git a/runtime/tests/test_gpu_census.c b/runtime/tests/test_gpu_census.c new file mode 100644 index 00000000..fe623fa3 --- /dev/null +++ b/runtime/tests/test_gpu_census.c @@ -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; +}