Bind full RAM diagnostics to the declared TAS return count

(cherry picked from commit 6f75d55234d1f901548a0a7c2eaf8cd2cce30feb)
This commit is contained in:
Pegasus
2026-09-09 13:01:12 +03:00
parent d5a063d2f7
commit 800f90507a
5 changed files with 106 additions and 5 deletions
+5
View File
@@ -789,6 +789,11 @@ if(BUILD_TESTING)
add_test(NAME tas_replay_recipe
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/test_tekken3.py")
# The Python driver compiles test_source_ram_page_probe.c at O0/O2.
add_test(NAME tas_ram_long_route
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_source_ram_page_probe.py"
--cc ${CMAKE_C_COMPILER})
endif()
endif()
+17 -3
View File
@@ -17,17 +17,31 @@ static uint64_t source_ram_page_hash(const uint8_t *bytes, unsigned count) {
static void source_ram_page_probe(unsigned frame, uint64_t cycle) {
static int initialized, enabled;
static FILE *stream;
static unsigned snapshots[32], snapshot_count;
static unsigned snapshots[32], snapshot_count, max_frames = 20000;
if (!initialized) {
initialized = 1;
const char *setting = getenv("PSX_SOURCE_RAM_PAGE_PROBE");
enabled = setting && strcmp(setting, "1") == 0;
const char *limit = getenv("PSX_SOURCE_RAM_MAX_FRAMES");
if (enabled && limit) {
/* A declared route may exceed the historical short-TAS bound.
* Parse without strtoul overflow or accepting signs/whitespace. */
unsigned value = 0;
if (!*limit) abort();
for (const char *p = limit; *p; ++p) {
if (*p < '0' || *p > '9' || value > 100000) abort();
value = value * 10 + (unsigned)(*p - '0');
if (value > 1000000) abort();
}
if (!value) abort();
max_frames = value;
}
const char *cursor = getenv("PSX_SOURCE_RAM_SNAPSHOT_FRAMES");
while (enabled && cursor && *cursor) {
if (*cursor < '0' || *cursor > '9') abort();
char *end;
unsigned long value = strtoul(cursor, &end, 10);
if (end == cursor || value < 1 || value > 20000 || snapshot_count == 32 ||
if (end == cursor || value < 1 || value > max_frames || snapshot_count == 32 ||
(*end && *end != ',')) abort();
for (unsigned i = 0; i < snapshot_count; ++i)
if (snapshots[i] == value) abort();
@@ -37,7 +51,7 @@ static void source_ram_page_probe(unsigned frame, uint64_t cycle) {
}
}
if (!enabled) return;
if (frame < 1 || frame > 20000) abort();
if (frame < 1 || frame > max_frames) abort();
extern uint8_t *memory_get_ram_ptr(void);
const uint8_t *ram = memory_get_ram_ptr();
char path[4096];
@@ -0,0 +1,14 @@
/* Exercise the production diagnostic with synthetic RAM, including long movies. */
#include "source_ram_page_probe.h"
#include <signal.h>
static uint8_t test_ram[2097152];
uint8_t *memory_get_ram_ptr(void) { return test_ram; }
static void expected_abort(int ignored) { (void)ignored; exit(4); }
int main(int argc, char **argv) {
signal(SIGABRT, expected_abort);
if (argc != 2) return 2;
test_ram[0] = 0x34;
test_ram[2097151] = 0xab;
source_ram_page_probe((unsigned)strtoul(argv[1], NULL, 10), UINT64_C(40500000000));
return 0;
}
@@ -0,0 +1,60 @@
"""Long-route coverage and fail-closed bounds using the actual RAM observer."""
import argparse
import os
from pathlib import Path
import subprocess
import tempfile
def fnv(data):
h = 14695981039346656037
for byte in data:
h = ((h ^ byte) * 1099511628211) & 0xffffffffffffffff
return f'{h:016X}'
def main():
p = argparse.ArgumentParser()
p.add_argument('--cc', default='gcc')
a = p.parse_args()
here = Path(__file__).resolve().parent
ram = bytearray(2097152)
ram[0], ram[-1] = 0x34, 0xab
zero = fnv(bytes(4096))
expected = [fnv(ram[:4096])] + [zero]*510 + [fnv(ram[-4096:])]
cases = [('legacy',None,20000,None,0),
('legacy_limit',None,20001,None,4),
('pepsiman','71806',71806,'71806',0),
('re1','227202',227202,'227202',0),
('bound','71806',71807,None,4),
('snapshot_bound','71806',1,'71807',4),
('duplicate','71806',1,'71806,71806',4),
('zero','0',1,None,4), ('negative','-1',1,None,4),
('junk','71806x',1,None,4), ('overflow','999999999999999999999',1,None,4),
('space',' 71806',1,None,4), ('over_cap','1000001',1,None,4)]
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary)
for opt in ['-O0','-O2']:
exe=root/('probe'+opt+('.exe' if os.name=='nt' else ''))
subprocess.run([a.cc,'-std=c11',opt,'-Wall','-Wextra','-Werror',
'-I'+str(here.parent/'include'),str(here/'test_source_ram_page_probe.c'),
'-o',str(exe)],check=True)
for name,limit,frame,snapshot,code in cases:
out=root/(name+opt)
out.mkdir()
env={k:v for k,v in os.environ.items() if not k.startswith('PSX_')}
env.update(PSX_SOURCE_RAM_PAGE_PROBE='1',PSX_INPUT_ROUTE_CAPTURE_DIR=str(out))
if limit is not None: env['PSX_SOURCE_RAM_MAX_FRAMES']=limit
if snapshot: env['PSX_SOURCE_RAM_SNAPSHOT_FRAMES']=snapshot
result=subprocess.run([str(exe),str(frame)],env=env,capture_output=True,timeout=20)
assert result.returncode==code,(opt,name,result.returncode,code,result.stderr)
if code==0:
rows=(out/'ram-pages.tsv').read_text().splitlines()
assert len(rows)==3,(name,len(rows))
row=rows[2].split('\t')
assert row[:2]==[str(frame),'40500000000'] and row[2:]==expected,name
if snapshot:
assert (out/f'ram-frame-{frame:06d}.bin').read_bytes()==ram,name
else:
assert not (out/'ram-pages.tsv').exists(),name
print(f'PASS: {len(cases)*2} production RAM observer cases; all 512 pages, clocks and late raw snapshots')
if __name__=='__main__': main()
+10 -2
View File
@@ -149,8 +149,8 @@ def main():
raise ValueError('RAM page probe requires source raster frontend boundaries')
if args.ram_snapshot_frame and (not args.ram_page_probe or len(args.ram_snapshot_frame)>32 or
len(set(args.ram_snapshot_frame))!=len(args.ram_snapshot_frame) or
any(f<1 or f>20000 for f in args.ram_snapshot_frame)):
raise ValueError('RAM snapshots require page probe and1..32 unique frames in1..20000')
any(f<1 for f in args.ram_snapshot_frame)):
raise ValueError('RAM snapshots require page probe and 1..32 unique positive frames')
if args.timer2_model!="default" and args.timer1_model!="octoshock-2.2.2":
raise ValueError("timer2 model requires source timer1 model")
if args.timer1_model!="default" and args.field_model!="octoshock-2.2.2-ntsc-raster":
@@ -187,6 +187,13 @@ def main():
if clock_tape:
paths['cd_source_clock_tape'] = Path(clock_tape['path'])
identity = route_identity(paths["route"])
# The completion hook exits before the final return observer. Bind every
# enabled RAM capture to the exact declared number of observed returns.
ram_returns = identity['frames'] + args.neutral_tail - 1
if args.ram_page_probe and not 1 <= ram_returns <= 1000000:
raise ValueError('RAM capture requires 1..1000000 declared completed returns')
if any(frame > ram_returns for frame in args.ram_snapshot_frame):
raise ValueError('RAM snapshot exceeds the declared completed-return boundary')
update_profile = None
update_contexts = None
context_values = None
@@ -328,6 +335,7 @@ p2_mode = "digital"
selected_env['PSX_SOURCE_CPU_RETURN_PROBE']='1'
if args.ram_page_probe:
selected_env['PSX_SOURCE_RAM_PAGE_PROBE']='1'
selected_env['PSX_SOURCE_RAM_MAX_FRAMES']=str(ram_returns)
if args.ram_snapshot_frame:
selected_env['PSX_SOURCE_RAM_SNAPSHOT_FRAMES']=','.join(map(str,args.ram_snapshot_frame))
if args.update_predictor != "gate":