Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions runtime/include/bios_hle.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion runtime/include/gpu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 17 additions & 1 deletion runtime/src/bios_hle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) ──── */

Expand Down
15 changes: 9 additions & 6 deletions runtime/src/gpu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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 */
}
Expand Down
44 changes: 44 additions & 0 deletions runtime/tests/test_bios_hle_recording.c
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions runtime/tests/test_bios_hle_recording.py
Original file line number Diff line number Diff line change
@@ -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 changes: 43 additions & 0 deletions runtime/tests/test_gpu_census.c
Original file line number Diff line number Diff line change
@@ -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;
}
Loading