Skip to content

Commit f235da5

Browse files
JoeMattclaude
andcommitted
acid: scaffold synthetic-ROM test toolkit
Establishes the directory structure, boot/signature conventions, build glue and runner harness for a future suite of focused acid-test ROMs. Per the user's earlier idea: ship small, open-source Jaguar ROMs that hammer specific hardware behaviour and report pass/fail to the host via a fixed RAM signature, so we can: * benchmark deterministically without depending on commercial ROMs (which we cannot ship), and * exhaustively cover feature axes (every blitter pixsize / phrase mode / Z mode etc.) instead of relying on whatever combinations the games we happen to test exercise. What this commit ships: * `test/acid/README.md` -- design doc, signature convention, vasm install steps, how to write a new test. * `test/acid/include/acid_test.s` -- ACID_INIT / ACID_PASS / ACID_FAIL macros that write a 4-word signature to RAM at $100..$10F. * `test/acid/include/jaguar_header.s` -- minimal cart header + entry vector; relies on the existing emulator-side BIOS auth bypass. * `test/acid/tests/blitter/copy_simple.s` -- first source-form test (trivial 8-phrase blitter copy round-trip). Serves as the canonical template for new tests. * `test/acid/Makefile` -- assembles `tests/**/*.s` into `.jag` ROMs using vasm (motorola syntax + 68K backend); pads each to 1 MB so retro_load_game treats them as normal carts. If `vasmm68k_mot` is not on $PATH the assemble step is skipped with a one-line warning (so CI still validates that the runner harness compiles). * `test/acid/run.c` -- harness: dlopens a libretro core, loads a .jag, runs N frames, reads the acid signature out of SYSTEM_RAM and prints PASS / FAIL / NOT-RUN-YET with diagnostic codes. Exit 0 = pass, 1 = fail or not-run, 2 = harness error. * `Makefile` -- `make acid` builds the core and runs every assembled test through the harness. No-op if vasm is absent. * `.gitignore` -- excludes `acid_run` and `tests/**/*.jag` build outputs. Caveats / known follow-ups: * The boot stub in `jaguar_header.s` is a best-effort transcription of the standard cart layout but has *not* yet been verified to boot inside the emulator. Once a host with vasm is available we'll bring up `copy_simple.jag` end-to-end and adjust the header / authentication-bypass interaction as needed. * No tests are pre-built into the repo yet; every category directory (`blitter/`, `gpu/`, `dsp/`, `op/`, `timing/`) is empty save the proof-of-concept blitter test. Tests land in follow-up PRs. * `vasm` isn't yet wired into CI -- when we're confident the toolkit works end-to-end we'll add a CI job that builds vasm from source and runs `make acid` so regressions get caught automatically. Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent 57741b4 commit f235da5

8 files changed

Lines changed: 720 additions & 1 deletion

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ test/lldb_*.py
6161
/test/tools/test_blitter_compare
6262
/test/tools/test_screenshot
6363
test/tools/build/
64+
65+
# Acid-test build outputs
66+
test/acid/acid_run
67+
test/acid/tests/**/*.jag

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ test/tools/test_memory_map: test/tools/test_memory_map.c
857857
-o $@ test/tools/test_memory_map.c -ldl
858858
endif
859859

860-
.PHONY: clean test lint coverage benchmark
860+
.PHONY: clean test lint coverage benchmark acid
861861
endif
862862

863863
lint:
@@ -905,6 +905,13 @@ benchmark:
905905
--warmup $(BENCH_WARMUP) --blitter $(BENCH_BLITTER) \
906906
$(if $(BENCH_STATE),--load-state "$(BENCH_STATE)")
907907

908+
# `make acid` -- builds the core and runs the synthetic acid-test ROMs
909+
# (see test/acid/README.md). Requires the vasm 68K assembler on $PATH;
910+
# if absent, the assemble step is skipped and only the runner harness
911+
# is built (so CI can still validate the harness compiles).
912+
acid: $(TARGET)
913+
$(MAKE) -C test/acid test CORE=$(abspath $(TARGET))
914+
908915
print-%:
909916
@echo '$*=$($*)'
910917

test/acid/Makefile

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#
2+
# test/acid/Makefile - assembles the synthetic acid-test ROMs.
3+
#
4+
# Toolchain: vasm (motorola syntax) + vlink. Get them from
5+
# http://sun.hasenbraten.de/vasm/ and http://sun.hasenbraten.de/vlink/.
6+
# Build vasm with `make CPU=m68k SYNTAX=mot`.
7+
#
8+
# If `vasmm68k_mot` is not on $PATH this Makefile prints a one-line
9+
# warning and skips the assemble step entirely.
10+
#
11+
12+
SRCDIR := tests
13+
INCDIR := include
14+
RUNNER_BIN := acid_run
15+
16+
VASM ?= vasmm68k_mot
17+
18+
VASM_FLAGS := -Fbin -m68000 -spaces -I$(INCDIR)
19+
20+
SOURCES := $(shell find $(SRCDIR) -name '*.s' -type f 2>/dev/null)
21+
ROMS := $(SOURCES:.s=.jag)
22+
23+
VASM_PRESENT := $(shell command -v $(VASM) 2>/dev/null)
24+
25+
ifeq ($(VASM_PRESENT),)
26+
ROMS_TO_BUILD :=
27+
else
28+
ROMS_TO_BUILD := $(ROMS)
29+
endif
30+
31+
.PHONY: all clean check-vasm test
32+
33+
all: $(RUNNER_BIN) $(ROMS_TO_BUILD)
34+
@$(MAKE) -s check-vasm
35+
36+
check-vasm:
37+
ifeq ($(VASM_PRESENT),)
38+
@echo "** $(VASM) not found on PATH"
39+
@echo "** Skipped assembling acid-test ROMs."
40+
@echo "** See test/acid/README.md for vasm install instructions."
41+
endif
42+
43+
# .s -> .jag: assemble flat binary at the program's org address ($800000),
44+
# then pad to 1 MB so retro_load_game sees a normal-sized cart.
45+
%.jag: %.s
46+
@mkdir -p $(dir $@)
47+
$(VASM) $(VASM_FLAGS) -o $@ $<
48+
@actual=$$(wc -c < $@); \
49+
target=1048576; \
50+
if [ $$actual -lt $$target ]; then \
51+
dd if=/dev/zero bs=1 count=$$(($$target - $$actual)) >> $@ 2>/dev/null; \
52+
fi
53+
@echo " ASM $< -> $@ ($$(wc -c < $@) bytes)"
54+
55+
clean:
56+
rm -f $(ROMS) $(RUNNER_BIN)
57+
58+
# Build the harness (separate from the .jag ROMs themselves).
59+
$(RUNNER_BIN): run.c
60+
$(CC) -O2 -Wall -std=c99 \
61+
-I../../libretro-common/include \
62+
-o $@ $< \
63+
$(if $(filter Linux,$(shell uname -s)),-ldl)
64+
65+
# Run all built tests through the harness. CORE points at the libretro
66+
# core .dylib/.so (defaults to the project root build).
67+
CORE ?= $(firstword $(wildcard ../../virtualjaguar_libretro.dylib ../../virtualjaguar_libretro.so))
68+
69+
test: all
70+
@if [ -z "$(CORE)" ]; then \
71+
echo "ERROR: set CORE=path/to/virtualjaguar_libretro.{dylib,so}"; \
72+
exit 2; \
73+
fi
74+
@if [ -z "$(ROMS_TO_BUILD)" ]; then \
75+
echo "Nothing to run (no .jag ROMs assembled)."; \
76+
exit 0; \
77+
fi
78+
@fail=0; total=0; \
79+
for rom in $(ROMS_TO_BUILD); do \
80+
total=$$((total+1)); \
81+
if ! ./$(RUNNER_BIN) "$(CORE)" "$$rom"; then fail=$$((fail+1)); fi; \
82+
done; \
83+
echo "----"; \
84+
echo "Acid tests: $$((total-fail)) / $$total passed"; \
85+
exit $$fail

test/acid/README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Acid-test ROM toolkit
2+
3+
Synthetic Jaguar ROMs that exercise specific hardware corners --
4+
blitter modes, GPU/DSP cross-talk, beam chasing, OP scenarios -- and
5+
report pass/fail to the host via a fixed RAM signature.
6+
7+
The motivation is two-fold:
8+
9+
1. **Reproducible perf benchmarks** that don't depend on commercial ROMs
10+
(which we can't ship). Each acid test is small (typically <8 KB),
11+
open-source, and exercises a single feature so we can attribute
12+
regressions cleanly.
13+
2. **Bug-finding under stress.** Commercial games hit wide combinations
14+
of features, but only the combinations *they happen to use*. Acid
15+
tests exhaustively walk a feature axis (every pixsize, every
16+
phrase/non-phrase, every Z-mode) and catch divergence between fast
17+
and accurate blitters, between our implementation and the hardware
18+
reference, and between successive emulator versions.
19+
20+
Status: **early scaffolding.** Runner + build infrastructure landed,
21+
first source-form test landed, vasm dependency documented but optional
22+
(CI builds skip the assemble step when vasm is absent).
23+
24+
## Layout
25+
26+
```
27+
test/acid/
28+
README.md -- this file
29+
Makefile -- assembles tests/*.s into .jag ROMs (vasm)
30+
run.c -- harness: dlopen core, load ROM, read signature
31+
include/
32+
jaguar_header.s -- minimal Jaguar cart header + entry vector
33+
acid_test.s -- pass/fail signature macros
34+
tests/
35+
blitter/ -- blitter mode matrix
36+
gpu/ -- GPU coprocessor
37+
dsp/ -- DSP coprocessor
38+
op/ -- Object Processor
39+
timing/ -- VC/VP, halfline, beam chasing
40+
```
41+
42+
## How a test reports its result
43+
44+
Tests write a four-word "acid signature" block at fixed RAM offset
45+
`0x100` (low main-RAM, well below the cart base and any normal use).
46+
47+
```
48+
0x100: ACID_RESULT -- 0x12345678 PASS, 0xDEADBEEF FAIL,
49+
0x00000000 NOT-RUN-YET
50+
0x104: ACID_DETAIL -- test-specific error / sub-test code
51+
0x108: ACID_OBSERVED -- value the test actually got (on FAIL)
52+
0x10C: ACID_EXPECTED -- value the test was looking for
53+
```
54+
55+
The runner reads main-RAM via `retro_get_memory_data(SYSTEM_RAM)` after
56+
running N frames and prints PASS / FAIL with diagnostics.
57+
58+
## Building
59+
60+
The toolchain is **vasm** (motorola syntax + Jaguar GPU/DSP backends),
61+
with **vlink** for linking. Both are open source from
62+
http://sun.hasenbraten.de/vasm/.
63+
64+
```bash
65+
# macOS (build from source -- not in Homebrew):
66+
git clone http://sun.hasenbraten.de/vasm/release/vasm.tar.gz # or: curl -O
67+
cd vasm && make CPU=m68k SYNTAX=mot
68+
sudo install vasmm68k_mot /usr/local/bin/
69+
70+
git clone http://sun.hasenbraten.de/vlink/release/vlink.tar.gz
71+
cd vlink && make
72+
sudo install vlink /usr/local/bin/
73+
```
74+
75+
Linux: same source build, no package manager wrapper.
76+
77+
Then:
78+
79+
```bash
80+
cd test/acid && make # assembles all tests/*.s into *.jag
81+
make acid # from repo root: build core + tests + run
82+
```
83+
84+
If `vasmm68k_mot` is not on `$PATH`, the Makefile prints a one-line
85+
warning and skips the assemble step. Pre-built `.jag` ROMs are checked
86+
into `tests/<category>/prebuilt/` for the cases where we want CI to
87+
test against a known-good binary without depending on the assembler.
88+
89+
## Writing a new test
90+
91+
1. Pick a category (`blitter/`, `gpu/`, etc.) or add a new one.
92+
2. Drop a `<name>.s` file. Start from
93+
`tests/blitter/copy_simple.s` as a template.
94+
3. Include the acid header + the signature macros:
95+
```
96+
include "jaguar_header.s"
97+
include "acid_test.s"
98+
```
99+
4. Write your test. End with `ACID_PASS` or `ACID_FAIL detail,
100+
observed, expected`.
101+
5. Run `make` in `test/acid/`; the new test's `.jag` appears alongside.
102+
6. Run `./run <core> <name>.jag` to verify.
103+
104+
## Running
105+
106+
```bash
107+
# From repo root:
108+
make acid # build + run all tests
109+
test/acid/run ./virtualjaguar_libretro.dylib \
110+
test/acid/tests/blitter/copy_simple.jag # one test
111+
```
112+
113+
The runner exits 0 if all PASS, non-zero if any FAIL or NOT-RUN-YET.
114+
115+
## Future categories (not yet shipped)
116+
117+
- **Blitter mode matrix** -- every (pixsize, phrase_mode, gourd, gourz,
118+
bcompen, dcompen) combination, fast vs accurate divergence checks.
119+
- **GPU<->Blitter sync** -- GPU programs that issue a blit, poll BUSY,
120+
and verify dest data.
121+
- **DSP<->68K I2S** -- DSP fills SOR/SOL, 68K observes IRQ timing,
122+
measure jitter.
123+
- **OP edge cases** -- scaled bitmaps with ZP, branch objects, GPU-int
124+
objects, OP-list cycles.
125+
- **Beam chasing** -- VC/VP register reads at known scanline offsets,
126+
programmatic palette swaps mid-frame.
127+
- **Cycle stress** -- fixed-iteration GPU/DSP loops with predictable
128+
cycle counts, used to characterise our event-scheduler timing
129+
accuracy.
130+
131+
Each will land as its own focused test or test family.

test/acid/include/acid_test.s

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
;
2+
; acid_test.s - pass/fail signature macros.
3+
;
4+
; The host runner reads four 32-bit words at RAM offset $100..$10F:
5+
;
6+
; $100 ACID_RESULT $12345678 = pass
7+
; $DEADBEEF = fail
8+
; $00000000 = not-yet-run
9+
; $104 ACID_DETAIL test-specific code
10+
; $108 ACID_OBSERVED value the test got
11+
; $10C ACID_EXPECTED value the test expected
12+
;
13+
14+
ACID_BASE equ $100
15+
ACID_RESULT equ ACID_BASE+0
16+
ACID_DETAIL equ ACID_BASE+4
17+
ACID_OBSERVED equ ACID_BASE+8
18+
ACID_EXPECTED equ ACID_BASE+12
19+
20+
ACID_PASS_MAGIC equ $12345678
21+
ACID_FAIL_MAGIC equ $DEADBEEF
22+
23+
;
24+
; ACID_PASS - mark this test as passing and halt.
25+
; Clobbers d0/d1.
26+
;
27+
ACID_PASS macro
28+
move.l #ACID_PASS_MAGIC,d0
29+
move.l d0,ACID_RESULT.w
30+
bra.s .acid_halt\@
31+
.acid_halt\@: bra.s .acid_halt\@
32+
endm
33+
34+
;
35+
; ACID_FAIL - mark this test as failing and halt.
36+
; Args:
37+
; detail : 32-bit code (typically a sub-test ID)
38+
; observed: 32-bit value the test actually saw
39+
; expected: 32-bit value the test wanted
40+
; Clobbers d0/d1.
41+
;
42+
ACID_FAIL macro detail,observed,expected
43+
move.l #(\1),d0
44+
move.l d0,ACID_DETAIL.w
45+
move.l #(\2),d0
46+
move.l d0,ACID_OBSERVED.w
47+
move.l #(\3),d0
48+
move.l d0,ACID_EXPECTED.w
49+
move.l #ACID_FAIL_MAGIC,d0
50+
move.l d0,ACID_RESULT.w
51+
bra.s .acid_halt\@
52+
.acid_halt\@: bra.s .acid_halt\@
53+
endm
54+
55+
;
56+
; ACID_INIT - clear the signature block to NOT-RUN-YET. Call once
57+
; near the top of your test before doing any real work.
58+
; Clobbers d0/a0.
59+
;
60+
ACID_INIT macro
61+
lea ACID_BASE.w,a0
62+
moveq #0,d0
63+
move.l d0,(a0)+
64+
move.l d0,(a0)+
65+
move.l d0,(a0)+
66+
move.l d0,(a0)+
67+
endm

test/acid/include/jaguar_header.s

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
;
2+
; jaguar_header.s - minimal Jaguar cart header + entry vector.
3+
;
4+
; Layout:
5+
; $800000 ATARI tag ; bypassed by emulators that skip auth
6+
; $800400 jump table to entry ; standard Universal Header offset
7+
; $802000 user code begins here
8+
;
9+
; The harness loads the .jag at $800000 and the BIOS jumps to $802000
10+
; via the universal header at $800400. This is the same layout used by
11+
; Atari's tools and most homebrew. Authentication is bypassed inside
12+
; the core (the BIOS auth-loop handler in src/core/jaguar.c short-
13+
; circuits when a cart is present), so we don't need a real cart
14+
; signature.
15+
;
16+
; Each test should:
17+
; include "jaguar_header.s" ; this file
18+
; include "acid_test.s" ; pass/fail macros
19+
; org $802000
20+
; entry: ; <-- BIOS jumps here
21+
; ACID_INIT
22+
; ; ... your test code ...
23+
; ACID_PASS ; or ACID_FAIL ...,...,...
24+
;
25+
26+
;; ROM origin
27+
org $800000
28+
29+
;; Skunkboard / Universal Header preamble. Real carts
30+
;; have an "ATARI" tag and licence text here that the
31+
;; BIOS validates; we rely on the emulator skipping
32+
;; that check, so just pad to the entry vector.
33+
dc.b "ATARI APPROVED DATA HEADER ATRI ",0
34+
ds.b $800400-*,0
35+
36+
;; Universal Header entry vector at $800400.
37+
;; The Jaguar BIOS jumps through this to start the cart.
38+
jmp entry
39+
40+
;; Pad to the user code area.
41+
ds.b $802000-*,0

0 commit comments

Comments
 (0)