test(memory): integrate pinned public memory compatibility suite

Add --suite memory with verified source hashes, ARM eval checkpoint 0x08000350, and the r12 result assertion. Keep default ARM and explicit Thumb preparation unchanged. Extend offline adapter and CLI tests; retain public binaries outside tracked files.

The unmodified memory ROM passes in debug and release after 174 steps and 1478 nominal cycles, without changing core behavior. Document weak byte-write assertions and mode-selection caveats. Add original CPU-driven tests with exact sentinels, mode/mirror boundaries, byte lanes, forced blank, readback, and nominal device timing.

Validated on Darwin arm64: 851 workspace tests in debug and release; 782 core/demo tests separately; 30 Python tests; all three pinned public suites in both profiles; native ROM windows; all ten demo smoke tests; fmt, clippy, rustdoc, and local documentation links.
This commit is contained in:
0xrsydn 2026-10-05 21:19:10 +07:00
commit 661f00d682
14 changed files with 351 additions and 19 deletions

View file

@ -35,6 +35,7 @@ Window smoke tests (`--*-smoke-test`) need a logged-in desktop session. Details
| --- | --- | | --- | --- |
| ARM/Thumb instructions, modes, exceptions, timing | [docs/hardware/cpu.md](docs/hardware/cpu.md) | | ARM/Thumb instructions, modes, exceptions, timing | [docs/hardware/cpu.md](docs/hardware/cpu.md) |
| Pinned public ARM/Thumb tests and compatibility results | [docs/public-arm-tests.md](docs/public-arm-tests.md), [docs/public-thumb-tests.md](docs/public-thumb-tests.md) | | Pinned public ARM/Thumb tests and compatibility results | [docs/public-arm-tests.md](docs/public-arm-tests.md), [docs/public-thumb-tests.md](docs/public-thumb-tests.md) |
| Pinned public memory tests and video-byte coverage limits | [docs/public-memory-tests.md](docs/public-memory-tests.md) |
| ROM test suites, assertions, and JSON reports | [docs/rom-tests.md](docs/rom-tests.md) | | ROM test suites, assertions, and JSON reports | [docs/rom-tests.md](docs/rom-tests.md) |
| ROM files, window execution, and terminal diagnostics | [docs/hardware/cartridge.md](docs/hardware/cartridge.md) | | ROM files, window execution, and terminal diagnostics | [docs/hardware/cartridge.md](docs/hardware/cartridge.md) |
| BIOS services (SWI) | [docs/hardware/bios.md](docs/hardware/bios.md) | | BIOS services (SWI) | [docs/hardware/bios.md](docs/hardware/bios.md) |

View file

@ -43,6 +43,7 @@ Use `--test-suite suite.json` for [repeatable headless ROM assertions](docs/rom-
| [docs/rom-tests.md](docs/rom-tests.md) | Headless ROM suites, completion checks, assertions, and JSON reports | | [docs/rom-tests.md](docs/rom-tests.md) | Headless ROM suites, completion checks, assertions, and JSON reports |
| [docs/public-arm-tests.md](docs/public-arm-tests.md) | Pinned public ARM test, preparation command, results, and limits | | [docs/public-arm-tests.md](docs/public-arm-tests.md) | Pinned public ARM test, preparation command, results, and limits |
| [docs/public-thumb-tests.md](docs/public-thumb-tests.md) | Pinned public Thumb test, result protocol, and compatibility baseline | | [docs/public-thumb-tests.md](docs/public-thumb-tests.md) | Pinned public Thumb test, result protocol, and compatibility baseline |
| [docs/public-memory-tests.md](docs/public-memory-tests.md) | Pinned public memory test, results, and video-byte coverage limits |
| [docs/architecture.md](docs/architecture.md) | Crate boundaries and a file-by-file map | | [docs/architecture.md](docs/architecture.md) | Crate boundaries and a file-by-file map |
| [docs/hardware/cpu.md](docs/hardware/cpu.md) | ARM/Thumb execution rules, modes, exceptions, and timing | | [docs/hardware/cpu.md](docs/hardware/cpu.md) | ARM/Thumb execution rules, modes, exceptions, and timing |
| [docs/hardware/cartridge.md](docs/hardware/cartridge.md) | ROM loading, window/input controls, original ROM generator, and diagnostics | | [docs/hardware/cartridge.md](docs/hardware/cartridge.md) | ROM loading, window/input controls, original ROM generator, and diagnostics |

View file

@ -0,0 +1,157 @@
//! Original CPU-driven checks that strengthen the public memory ROM's byte tests.
use gba_core::{
cpu::Cpu,
io::{DISPCNT, TIMER_BASE},
machine::{Machine, StepKind},
memory::{Memory, OAM_START, PALETTE_START, ROM_START, VRAM_START},
timing::StepTiming,
};
const SENTINEL: u16 = 0xa55a;
fn store_program(address: u32, value: u32, store: u32, control: u16) -> Machine {
let words = [
0xe59f_0010, // LDR r0, [pc, #16]: full source value
0xe59f_1010, // LDR r1, [pc, #16]: destination
store,
0xe3c1_3001, // BIC r3, r1, #1: aligned halfword readback address
0xe1d3_20b0, // LDRH r2, [r3]
0xeaff_fffe, // B .
value,
address,
];
let mut memory = Memory::new(words.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
memory.write16(DISPCNT, control).unwrap();
memory.write32(TIMER_BASE, 0x0080_0000).unwrap();
Machine::new(Cpu::new(ROM_START), memory)
}
fn execute_store(machine: &mut Machine, data_cycles: u32) {
for _ in 0..2 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
let before = machine.cycles();
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(
machine.last_timing(),
StepTiming {
code_cycles: 8, // Non-sequential ARM word in ROM window 0.
data_cycles,
internal_cycles: 0,
idle_cycles: 0,
}
);
assert_eq!(machine.cycles() - before, u64::from(8 + data_cycles));
assert_eq!(
machine.memory().read16(TIMER_BASE).unwrap(),
machine.cycles() as u16
);
assert_eq!(machine.cpu().pc(), ROM_START + 12);
assert_eq!(machine.cpu().cpsr(), 0x1f);
}
fn check_byte(address: u32, physical: u32, control: u16, duplicates: bool, value: u32) {
let mut machine = store_program(address, value, 0xe5c1_0000, control); // STRB r0, [r1]
let target = physical & !1;
machine.memory_mut().write16(target - 2, 0x1357).unwrap();
machine.memory_mut().write16(target, SENTINEL).unwrap();
machine.memory_mut().write16(target + 2, 0x2468).unwrap();
execute_store(&mut machine, 1); // Ignored stores still consume their data access.
let expected = if duplicates {
(value as u8 as u16) * 0x101
} else {
SENTINEL
};
assert_eq!(
machine.memory().read16(target).unwrap(),
expected,
"address={address:#010x} control={control:#06x} value={value:#010x}"
);
assert_eq!(machine.memory().read16(address & !1).unwrap(), expected);
assert_eq!(machine.memory().read16(target - 2).unwrap(), 0x1357);
assert_eq!(machine.memory().read16(target + 2).unwrap(), 0x2468);
for _ in 0..2 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
}
assert_eq!(machine.cpu().registers()[0], value);
assert_eq!(machine.cpu().registers()[1], address);
assert_eq!(machine.cpu().registers()[2], u32::from(expected));
assert_eq!(machine.cpu().cpsr(), 0x1f);
assert_eq!(machine.memory().read16(DISPCNT).unwrap(), control);
}
#[test]
fn cpu_strb_obeys_exact_vram_boundaries_in_every_supported_mode_and_mirror() {
// Explicit physical addresses and allowed mode classes, independent of vram_index.
// The final flag marks the 64..80 KiB area that accepts bytes only in bitmap modes.
let cases = [
(0x0600_0020, 0x0600_0020, true, false),
(0x0600_fffe, 0x0600_fffe, true, false),
(0x0601_0000, 0x0601_0000, false, true),
(0x0601_3ffe, 0x0601_3ffe, false, true),
(0x0601_4000, 0x0601_4000, false, false),
(0x0601_7ffe, 0x0601_7ffe, false, false),
(0x0601_8000, 0x0601_0000, false, true),
(0x0601_bffe, 0x0601_3ffe, false, true),
(0x0601_c000, 0x0601_4000, false, false),
(0x0601_fffe, 0x0601_7ffe, false, false),
(0x0602_0020, 0x0600_0020, true, false),
(0x06ff_fffe, 0x0601_7ffe, false, false),
];
for mode in 0..=5 {
for blank in [0, 0x80] {
for (address, physical, always, bitmap_only) in cases {
for low in 0..2 {
for value in [0x1234_5600, 0xaabb_cc01, 0x9988_77a5, 0xffff_ffff] {
check_byte(
address + low,
physical + low,
mode | blank,
always || (bitmap_only && mode >= 3),
value,
);
}
}
}
}
}
}
#[test]
fn cpu_strb_duplicates_palette_but_preserves_every_oam_byte() {
for (address, physical, duplicates) in [
(PALETTE_START + 0x20, PALETTE_START + 0x20, true),
(0x0500_0420, PALETTE_START + 0x20, true),
(0x05ff_fffe, PALETTE_START + 0x3fe, true),
(OAM_START + 0x20, OAM_START + 0x20, false),
(0x0700_0420, OAM_START + 0x20, false),
(0x07ff_fffe, OAM_START + 0x3fe, false),
] {
for mode in 0..=5 {
for low in 0..2 {
for value in [0, 1, 0x7f, 0x80, 0xff] {
check_byte(address + low, physical + low, mode, duplicates, value);
}
}
}
}
}
#[test]
fn cpu_halfword_and_word_stores_still_modify_obj_and_oam() {
for mode in 0..=5 {
for address in [VRAM_START + 0x10000, VRAM_START + 0x14000, OAM_START + 0x20] {
for (store, word) in [(0xe1c1_00b0, false), (0xe581_0000, true)] {
// STRH / STR
let mut machine = store_program(address, 0x1234_5678, store, mode);
machine.memory_mut().write32(address, 0xa55a_aa55).unwrap();
let data_cycles = if word && address < OAM_START { 2 } else { 1 };
execute_store(&mut machine, data_cycles);
assert_eq!(
machine.memory().read32(address).unwrap(),
if word { 0x1234_5678 } else { 0xa55a_5678 }
);
}
}
}
}

View file

@ -14,7 +14,7 @@ The repository is a Cargo workspace with three crates:
| Path | Purpose | | Path | Purpose |
| --- | --- | | --- | --- |
| `flake.nix`, `flake.lock` | Pinned Rust/Python development environment | | `flake.nix`, `flake.lock` | Pinned Rust/Python development environment |
| `tools/prepare_gba_tests.py`, `tools/gba-tests-{arm,thumb}.lock.json` | Hash-verified public ARM/Thumb source/ROM preparation and suite generation | | `tools/prepare_gba_tests.py`, `tools/gba-tests-{arm,thumb,memory}.lock.json` | Hash-verified public test source/ROM preparation and suite generation |
| `tools/test_prepare_gba_tests.py` | Offline adapter tests using original synthetic data | | `tools/test_prepare_gba_tests.py` | Offline adapter tests using original synthetic data |
| `.envrc` | Automatic environment loading through direnv | | `.envrc` | Automatic environment loading through direnv |
| `AGENTS.md` | Short orientation for coding agents: crate map, commands, and where docs live | | `AGENTS.md` | Short orientation for coding agents: crate map, commands, and where docs live |
@ -98,6 +98,7 @@ The repository is a Cargo workspace with three crates:
| `crates/desktop/tests/rom_cli.rs` | File loading and exit-status checks; opt-in native ROM-window integration test | | `crates/desktop/tests/rom_cli.rs` | File loading and exit-status checks; opt-in native ROM-window integration test |
| `crates/gba-core/tests/core.rs` | CPU integration tests and the complete demo | | `crates/gba-core/tests/core.rs` | CPU integration tests and the complete demo |
| `crates/gba-core/tests/memory.rs` | Memory widths, alignment, errors, and mirrors | | `crates/gba-core/tests/memory.rs` | Memory widths, alignment, errors, and mirrors |
| `crates/gba-core/tests/video_bus_cpu.rs` | Original CPU video-byte writes, exact sentinels, mirror/mode boundaries, readback, and nominal store timing |
| `crates/gba-core/tests/exceptions.rs` | Exception demo, BIOS mapping, and reset-vector execution | | `crates/gba-core/tests/exceptions.rs` | Exception demo, BIOS mapping, and reset-vector execution |
| `crates/gba-core/tests/timers.rs` | Timer rules, interrupt registers, and a cycle-by-cycle reference | | `crates/gba-core/tests/timers.rs` | Timer rules, interrupt registers, and a cycle-by-cycle reference |
| `crates/gba-core/tests/machine.rs` | Device IRQ entry/return, clock policy, and I/O failure atomicity | | `crates/gba-core/tests/machine.rs` | Device IRQ entry/return, clock policy, and I/O failure atomicity |

View file

@ -111,7 +111,8 @@ Original regressions separately check the returned word, byte lanes, rotation, s
See the bounded [ARM open-bus implementation](hardware/cpu.md#arm-unused-memory-data-reads). See the bounded [ARM open-bus implementation](hardware/cpu.md#arm-unused-memory-data-reads).
This is a passing result for this pinned ARM ROM, not proof of complete instruction, memory, or timing compatibility. This is a passing result for this pinned ARM ROM, not proof of complete instruction, memory, or timing compatibility.
The [pinned public Thumb ROM](public-thumb-tests.md) also passes its verified checkpoint. The [pinned public Thumb ROM](public-thumb-tests.md) also passes its verified checkpoint.
No public timing, memory, BIOS, graphics, or unsafe suite has been claimed as passing. The [pinned public memory ROM](public-memory-tests.md) passes its limited mirror and video-byte checks.
No public timing, BIOS, graphics, save, or unsafe suite has been claimed as passing.
These results do not establish Pokémon Emerald compatibility. These results do not establish Pokémon Emerald compatibility.
## References ## References

View file

@ -0,0 +1,94 @@
# Pinned public memory test
The headless runner executes the unmodified memory ROM from [jsmolka/gba-tests](https://github.com/jsmolka/gba-tests).
It passes its result checkpoint on Darwin arm64 in debug and release builds without a core behavior change.
This suite samples memory mirrors and video-byte writes. It does not establish complete memory-bus compatibility.
## Prepare and run
```sh
direnv exec . python3 tools/prepare_gba_tests.py /tmp/gba-public-memory --suite memory
direnv exec . cargo run --locked --release -- --test-suite /tmp/gba-public-memory/suite.json > /tmp/gba-public-memory-report.json
```
Use a new directory outside the repository or inside ignored `roms/`. Its parent directory must exist.
Every selected source and binary file has a pinned byte count and SHA-256 digest.
The adapter checks all files before creating output, retains the upstream license, and writes `suite.json` and `source-lock.json`.
No ROM patch, source rewrite, header replacement, test skip, or result adjustment occurs.
Downloaded ROMs and header sources must not enter tracked files.
Offline preparation applies the same checks:
```sh
direnv exec . python3 tools/prepare_gba_tests.py /tmp/gba-public-memory-offline --suite memory --source-dir /path/to/gba-tests
```
The default remains the [ARM suite](public-arm-tests.md). `--suite thumb` selects the [Thumb suite](public-thumb-tests.md).
Each invocation prepares one suite. Normal Rust and Python tests do not download public files.
Python comes from Nix and requires no extra packages. Preparation alone is not a compatibility verdict.
## Provenance and result protocol
- Repository: `jsmolka/gba-tests`.
- Revision: `a7113b67e63f83a9b321696ddd7042ccfad6c881`.
- Upstream project license: MIT, copyright Julian Smolka; retained with downloaded files.
- ROM: `memory/memory.gba`, 2,172 bytes.
- SHA-256: `21024fb6aae6343f5f0466dd54e3149de1fbeb23f78e7d85a015c983684d2f87`.
- Selected file hashes and sizes: [`tools/gba-tests-memory.lock.json`](../tools/gba-tests-memory.lock.json).
The adapter uses the committed upstream binary. No FASMARM rebuild or source-to-binary reproduction was performed on Darwin.
Startup initializes Mode 4 display state and sets r12 to zero.
Failure paths set r12 to their test number and branch to `eval`.
Successful execution reaches `eval` with r12 still zero.
The source exits and their binary ARM branch targets were checked against this address.
`eval` is `0x08000350`, in ARM state. Its first word is `0xe92d0003`, the shared `m_vsync` register save.
Preparation checks this word in addition to whole-file hashes.
The manifest stops before executing `eval` and requires `r12 == 0`.
This runs startup and all memory tests without depending on result rendering, VBlank polling, or BIOS number formatting.
The budget is 1,000,000 successful machine steps, including original BIOS boot.
Diagnostics, timeouts, STOP, and failed assertions remain failures under the [runner's normal rules](rom-tests.md).
## Observed result on Darwin arm64
- The ROM reaches `0x08000350` after 174 successful steps and 1,478 nominal cycles.
- The CPU is in System/ARM state, with CPSR `0x6000001f` and r12 = 0.
- Debug and release builds exit zero with `reason: checkpoint`, no error, and the result assertion passing.
- The existing ARM and Thumb suites also retain their passing results.
- No emulator behavior change was needed for this result.
## Coverage and limits
The mirror tests sample external RAM, internal RAM, palette RAM, video RAM, object attribute memory, and both alternate cartridge windows.
The byte-write tests sample palette duplication, background video RAM duplication, and ignored object-memory writes.
They do not cover open bus, BIOS protection, save hardware, unused I/O, memory timing, or display-bus contention.
The pinned source also has limits that weaken its video-byte checks:
- Tests 50–52 reject one exact byte-sized value, rather than requiring the complete prior word to remain unchanged.
An incorrect duplicated-halfword write can therefore escape these checks.
- Startup selects Mode 4. Test 51 applies `ORR #3` to that mode, selecting reserved Mode 7 rather than a supported bitmap mode.
- Test 52 clears only mode bits 0–1. This leaves Mode 4 selected rather than selecting a tile mode.
- Test 5 explicitly selects Mode 0 for its upper video RAM mirror check. It does not validate every mirror in every mode.
These source behaviors are retained, not patched to make the suite stronger or easier to pass.
Passing this ROM must not be described as exhaustive video-memory validation.
Original CPU-driven regressions in `crates/gba-core/tests/video_bus_cpu.rs` provide separate, stronger byte-write checks.
They select each supported mode explicitly and require the exact duplicated halfword or unchanged nonzero sentinel.
They cover both byte lanes, physical and mirrored addresses, tile/bitmap boundaries, forced blank, and CPU readback.
Adjacent halfwords must remain unchanged. Ignored stores still pay their nominal data-access cost and advance devices.
Halfword and word stores still modify object video RAM and object attribute memory.
These are implementation regressions based on documented rules, not new hardware measurements.
No public BIOS, timing, graphics, save, or unsafe suite is claimed as passing.
Pokémon Emerald compatibility remains unverified.
## References
- [Pinned memory entry and result flow](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/memory/memory.asm).
- [Memory mirror checks](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/memory/mirrors.asm).
- [Video-byte checks and mode selection](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/memory/video_strb.asm).
- [Display initialization](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/lib/text.asm) and [shared result macros](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/lib/macros.inc).
- [GBATEK memory mirrors and video-byte writes](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm).
- [Pinned license](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/LICENSE).

View file

@ -81,7 +81,8 @@ They cover all low base registers, banked stack pointers, code/data alignment, R
Existing empty-list expectations that incorrectly required PC+4 were corrected; empty loads and ARM stores retain their behavior. Existing empty-list expectations that incorrectly required PC+4 were corrected; empty loads and ARM stores retain their behavior.
See [CPU transfer semantics](hardware/cpu.md#instruction-demo-and-execution-rules). See [CPU transfer semantics](hardware/cpu.md#instruction-demo-and-execution-rules).
This proves only the pinned suites' checkpoints and assertions, not complete CPU, bus, or timing compatibility. This proves only the pinned suites' checkpoints and assertions, not complete CPU, bus, or timing compatibility.
No public timing, memory, BIOS, graphics, or unsafe suite is claimed as passing. The [pinned public memory ROM](public-memory-tests.md) also passes, with documented video-byte coverage limits.
No public timing, BIOS, graphics, save, or unsafe suite is claimed as passing.
These results do not establish Pokémon Emerald compatibility. These results do not establish Pokémon Emerald compatibility.
## References ## References

View file

@ -66,6 +66,8 @@ Hardware references used for the core:
- [mGBA ARM instruction implementation](https://github.com/mgba-emu/mgba/blob/master/src/arm/isa-arm.c), reviewed for Rd=15 test/compare SPSR restoration, User/System fallback, and no result write/refill. Also reviewed its word/byte/halfword/signed load ordering: writeback precedes the destination result. - [mGBA ARM instruction implementation](https://github.com/mgba-emu/mgba/blob/master/src/arm/isa-arm.c), reviewed for Rd=15 test/compare SPSR restoration, User/System fallback, and no result write/refill. Also reviewed its word/byte/halfword/signed load ordering: writeback precedes the destination result.
- [jsmolka/gba-tests](https://github.com/jsmolka/gba-tests/tree/a7113b67e63f83a9b321696ddd7042ccfad6c881), pinned for independent ARM testing. Reviewed its MIT license, startup, result macros, compare/status tests, and load/writeback alias tests. - [jsmolka/gba-tests](https://github.com/jsmolka/gba-tests/tree/a7113b67e63f83a9b321696ddd7042ccfad6c881), pinned for independent ARM testing. Reviewed its MIT license, startup, result macros, compare/status tests, and load/writeback alias tests.
- [Pinned jsmolka Thumb entry](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/thumb/thumb.asm), [branch tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/thumb/branches.asm), and [memory tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/thumb/memory.asm), reviewed for r7 failure IDs, success-marker reset, the Thumb-to-ARM evaluation bridge, and empty-list transfer expectations. - [Pinned jsmolka Thumb entry](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/thumb/thumb.asm), [branch tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/thumb/branches.asm), and [memory tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/thumb/memory.asm), reviewed for r7 failure IDs, success-marker reset, the Thumb-to-ARM evaluation bridge, and empty-list transfer expectations.
- [Pinned jsmolka memory entry](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/memory/memory.asm), [mirror tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/memory/mirrors.asm), and [video-byte tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/memory/video_strb.asm), reviewed for the r12 checkpoint, binary exit targets, limited byte-write assertions, and mode-selection caveats.
- [gbadoc memory layout](https://gbadev.net/gbadoc/memory.html), consulted for VRAM mirror ranges alongside GBATEK's video-memory layout and byte-write rules.
- [armwrestler-gba-fixed](https://github.com/destoer/armwrestler-gba-fixed) and [arm7wrestler](https://github.com/Arisotura/arm7wrestler), considered as test sources; not integrated. - [armwrestler-gba-fixed](https://github.com/destoer/armwrestler-gba-fixed) and [arm7wrestler](https://github.com/Arisotura/arm7wrestler), considered as test sources; not integrated.
- [SingleStepTests ARM7TDMI](https://github.com/SingleStepTests/ARM7TDMI), considered as an experimental, emulator-generated test source; not integrated. - [SingleStepTests ARM7TDMI](https://github.com/SingleStepTests/ARM7TDMI), considered as an experimental, emulator-generated test source; not integrated.
- [GBATEK ARM single data transfers](https://problemkaputt.de/gbatek-arm-opcodes-memory-single-data-transfer-ldr-str-pld.htm), for address calculation, indexing, load extension, and nominal timing. - [GBATEK ARM single data transfers](https://problemkaputt.de/gbatek-arm-opcodes-memory-single-data-transfer-ldr-str-pld.htm), for address calculation, indexing, load extension, and nominal timing.

View file

@ -4,8 +4,9 @@ Use `--test-suite PATH.json` to run repeatable ROM checks without opening a wind
Each case has a machine-step budget, a completion address, and explicit assertions. Each case has a machine-step budget, a completion address, and explicit assertions.
A timeout never counts as a pass. Existing `--rom ... --steps ...` remains a diagnostic mode, not a test verdict. A timeout never counts as a pass. Existing `--rom ... --steps ...` remains a diagnostic mode, not a test verdict.
Original programs cover the runner itself. Pinned public [ARM](public-arm-tests.md) and [Thumb](public-thumb-tests.md) ROMs provide independent compatibility results. Original programs cover the runner itself. Pinned public [ARM](public-arm-tests.md), [Thumb](public-thumb-tests.md), and [memory](public-memory-tests.md) ROMs provide independent compatibility results.
Both pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds. These pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds.
The memory ROM has documented assertion and mode-selection limits; a pass is not exhaustive memory validation.
Passing the runner's unit tests does not mean an external suite passes. Passing the runner's unit tests does not mean an external suite passes.
A passing suite establishes only its specified checkpoints and assertions. It does not establish Emerald compatibility. A passing suite establishes only its specified checkpoints and assertions. It does not establish Emerald compatibility.
@ -134,7 +135,8 @@ They do not hash ROM contents; record source revisions and file checksums alongs
The first public integration is [jsmolka/gba-tests ARM](public-arm-tests.md), with a verified result-register checkpoint. The first public integration is [jsmolka/gba-tests ARM](public-arm-tests.md), with a verified result-register checkpoint.
The pinned ARM ROM now passes this checkpoint on Darwin arm64. Historical failures remain documented. The pinned ARM ROM now passes this checkpoint on Darwin arm64. Historical failures remain documented.
The [pinned Thumb ROM](public-thumb-tests.md) also passes its verified r7 checkpoint after the empty-list store correction. The [pinned Thumb ROM](public-thumb-tests.md) also passes its verified r7 checkpoint after the empty-list store correction.
Next, integrate a pinned public memory suite with a verified result protocol. The [pinned memory ROM](public-memory-tests.md) also passes, without a core change and with explicit coverage limits.
Next, integrate a pinned public BIOS suite with a verified result protocol.
For further public ARM7TDMI/GBA tests, inspect their source, license, entry assumptions, and result protocol. For further public ARM7TDMI/GBA tests, inspect their source, license, entry assumptions, and result protocol.
Pin source revisions and build instructions before comparing emulator changes. Pin source revisions and build instructions before comparing emulator changes.
Adapt suites to verified completion addresses and result locations; do not guess them or treat timeouts as passes. Adapt suites to verified completion addresses and result locations; do not guess them or treat timeouts as passes.

View file

@ -67,8 +67,8 @@ Update this file when a feature lands or a limit is removed.
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP. - ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
- Optional ROM-window frame limits and an original input-test ROM generator. - Optional ROM-window frame limits and an original input-test ROM generator.
- Headless JSON ROM suites with per-case checkpoints, register/CPSR/memory assertions, bounded budgets, and structured failure reports. - Headless JSON ROM suites with per-case checkpoints, register/CPSR/memory assertions, bounded budgets, and structured failure reports.
- Hash-pinned preparation of public `jsmolka/gba-tests` ARM and Thumb ROMs, with verified result-register checkpoints. - Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, and memory ROMs, with verified result-register checkpoints.
- The pinned public ARM and Thumb ROMs pass their checkpoints on Darwin arm64 in debug and release builds. - These pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds; coverage limits remain documented.
- ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill. - ARM test/compare Rd=15 status restoration, without a PC result write or nominal refill.
- ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation. - ARM unused-memory data reads return a PC+8 word snapshot, with normal byte lanes and load rotation.
- Little-endian byte, halfword, and word reads/writes. - Little-endian byte, halfword, and word reads/writes.
@ -118,7 +118,8 @@ See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, an
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md). The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).
The pinned public ARM ROM passes its result checkpoint in debug and release builds. The pinned public ARM ROM passes its result checkpoint in debug and release builds.
The [pinned public Thumb ROM](public-thumb-tests.md) also passes after correcting its empty-list store PC value. The [pinned public Thumb ROM](public-thumb-tests.md) also passes after correcting its empty-list store PC value.
Other public suites remain unverified. These results do not establish full CPU compatibility. The [pinned public memory ROM](public-memory-tests.md) passes without a core change; its video-byte assertions have documented limits.
Other public suites remain unverified. These results do not establish full CPU or memory compatibility.
The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions. The runner requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions.
ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads). ARM data reads from unused address ranges use a bounded [PC+8 open-bus snapshot](hardware/cpu.md#arm-unused-memory-data-reads).
@ -131,7 +132,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
## Next steps ## Next steps
1. Add a pinned public memory suite with a verified result protocol, then address its first compatibility failure. 1. Add a pinned public BIOS suite with a verified result protocol, then address its first compatibility failure.
Extend open-bus support with verified Thumb region/alignment rules and bus history, rather than a blanket unmapped-read fallback. Extend open-bus support with verified Thumb region/alignment rules and bus history, rather than a blanket unmapped-read fallback.
2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. 3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.

View file

@ -51,6 +51,7 @@ Its tests verify manifest limits, whole-suite validation, exact step boundaries,
Additional tests cover DMA/IRQ accounting, HALT/STOP, diagnostic state, case isolation, repeatable JSON output, and failure exit status. Additional tests cover DMA/IRQ accounting, HALT/STOP, diagnostic state, case isolation, repeatable JSON output, and failure exit status.
These original tests validate the runner. The [public ARM adapter](public-arm-tests.md) provides a separate passing checkpoint result for its pinned ROM. These original tests validate the runner. The [public ARM adapter](public-arm-tests.md) provides a separate passing checkpoint result for its pinned ROM.
The [public Thumb adapter](public-thumb-tests.md) uses a separate lock and r7 assertion after its return to ARM state. The [public Thumb adapter](public-thumb-tests.md) uses a separate lock and r7 assertion after its return to ARM state.
The [public memory adapter](public-memory-tests.md) uses its own lock and an ARM-state r12 checkpoint.
Preparation tests use only synthetic original bytes and no network: Preparation tests use only synthetic original bytes and no network:
```sh ```sh
@ -58,7 +59,7 @@ direnv exec . python3 -B -m unittest discover -s tools -p 'test_*.py' -v
``` ```
They verify suite selection, pinned URLs, hashes, sizes, checkpoint validation, offline copies, destination safety, and non-overwriting output. They verify suite selection, pinned URLs, hashes, sizes, checkpoint validation, offline copies, destination safety, and non-overwriting output.
Thumb cases also verify the return-to-ARM bridge. Command-line tests cover default ARM selection, explicit Thumb selection, and errors. Thumb cases also verify the return-to-ARM bridge. Command-line tests cover default ARM selection, explicit Thumb/memory selection, and errors.
Compare/status CPU regressions verify saved flags/masks, bank switching, sequential PC/timing, User/System fallback, and atomic diagnostics. Compare/status CPU regressions verify saved flags/masks, bank switching, sequential PC/timing, User/System fallback, and atomic diagnostics.
Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping. Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping.
They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors. They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors.
@ -69,7 +70,12 @@ The pinned public Thumb ROM also reaches its ARM-state checkpoint with r7 = 0 in
Empty-list Thumb regressions cover PC+6, all low bases and processor modes, banked stacks, both code alignments, and unaligned data. Empty-list Thumb regressions cover PC+6, all low bases and processor modes, banked stacks, both code alignments, and unaligned data.
They check one-word writes, 64-byte writeback, ROM windows, the following PC operand, nominal timing, and atomic errors. They check one-word writes, 64-byte writeback, ROM windows, the following PC operand, nominal timing, and atomic errors.
Existing ARM empty-list and Thumb empty-load tests continue to pass. `PUSH {lr}` retains its normal link value. Existing ARM empty-list and Thumb empty-load tests continue to pass. `PUSH {lr}` retains its normal link value.
These results do not establish public timing, memory, graphics, BIOS, or commercial-game compatibility. The pinned public memory ROM also passes in both builds, but its video-byte assertions and mode selection have documented limits.
Separate original CPU tests check exact video-byte results across supported modes, mirrored addresses, byte lanes, and boundaries.
They require unchanged nonzero sentinels for ignored writes and unchanged adjacent halfwords for duplicated writes.
They also check forced blank, full source-value preservation, CPU halfword readback, nominal data costs, and device progress.
Halfword/word stores remain writable in object regions. No emulator behavior change was needed for these tests.
These results do not establish public timing, graphics, BIOS, save, or commercial-game compatibility.
ROM-runner tests use temporary files containing original instructions, without Nintendo assets or a desktop session. ROM-runner tests use temporary files containing original instructions, without Nintendo assets or a desktop session.
They check file-size boundaries, bounded reads, short reads, I/O errors, paths, symlinks, and read-only loading. They check file-size boundaries, bounded reads, short reads, I/O errors, paths, symlinks, and read-only loading.

View file

@ -0,0 +1,54 @@
{
"repository": "jsmolka/gba-tests",
"revision": "a7113b67e63f83a9b321696ddd7042ccfad6c881",
"license": "MIT",
"rom": "memory/memory.gba",
"completion_pc": "0x08000350",
"result_register": 12,
"files": {
"LICENSE": {
"sha256": "b59a9ed235d76752c977f04116bc72155a14af48176bdbf50d7e5c29fca35aa7",
"bytes": 1070
},
"README.md": {
"sha256": "488f50d9d9031c88cea4161fad69f6460209dd54d965645fa45cf5e972cc9062",
"bytes": 434
},
"memory/memory.asm": {
"sha256": "03c62d6ac9d72e65698f1f444780657998510c1c63ec8fe62905b20419a9559c",
"bytes": 500
},
"memory/memory.gba": {
"sha256": "21024fb6aae6343f5f0466dd54e3149de1fbeb23f78e7d85a015c983684d2f87",
"bytes": 2172
},
"memory/mirrors.asm": {
"sha256": "e5f0a092d1d20042169adecb0ed69fcdb07d82546812dca41aed5a1a9ac6d3ff",
"bytes": 2334
},
"memory/video_strb.asm": {
"sha256": "a57e5cdd37b85029ff14e7f6aab75056fe488b94d325d8a323b9400785863420",
"bytes": 1911
},
"lib/constants.inc": {
"sha256": "b4dd3f4760ac1ae7e99cdc7e255f8220eed666e1ab821b150561db58e5d3b7ea",
"bytes": 900
},
"lib/macros.inc": {
"sha256": "25b0ff553c4ca99d76ec82c8c9db874e25d6eed58e2e69fa27f1a0fe7a8c46d4",
"bytes": 2864
},
"lib/header.asm": {
"sha256": "e9d1e8c7b6382eeb1b7059423ec365e32911f702eadfa51804a2119438a87357",
"bytes": 1720
},
"lib/text.asm": {
"sha256": "77e579d6cf0db182fd24e8d420c8c0a81592d78197e8fb06accc8bcfb43db17e",
"bytes": 2288
},
"lib/glyphs.asm": {
"sha256": "538beb37f40b1fd56beed9253e0e9eab25444c748adba7440a2093f49e3cbcc9",
"bytes": 2545
}
}
}

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Prepare a hash-verified, pinned public ARM or Thumb test outside tracked paths. """Prepare a pinned public ARM, Thumb, or memory test outside tracked paths.
This adapter is original. Upstream sources, license, and ROM bytes are downloaded This adapter is original. Upstream sources, license, and ROM bytes are downloaded
only to the requested local directory. They are not embedded in this repository. only to the requested local directory. They are not embedded in this repository.
@ -15,7 +15,7 @@ from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
LOCKS = {suite: Path(__file__).with_name(f"gba-tests-{suite}.lock.json") LOCKS = {suite: Path(__file__).with_name(f"gba-tests-{suite}.lock.json")
for suite in ("arm", "thumb")} for suite in ("arm", "thumb", "memory")}
MAX_FILE_BYTES = 1024 * 1024 MAX_FILE_BYTES = 1024 * 1024
@ -64,7 +64,7 @@ def prepare(output, source=None, download=fetch, suite="arm"):
files[name] = data files[name] = data
# The source-defined eval label starts with m_vsync's register save. All # The source-defined eval label starts with m_vsync's register save. All
# failure exits arrive with the result register set (ARM r12, Thumb r7). # failure exits set the result register (ARM/memory r12, Thumb r7).
# Thumb tests switch back to ARM before eval. Success leaves the result zero. # Thumb tests switch back to ARM before eval. Success leaves the result zero.
# Hash pinning is authoritative; these checks catch adapter-offset mistakes. # Hash pinning is authoritative; these checks catch adapter-offset mistakes.
pc = int(lock["completion_pc"], 16) pc = int(lock["completion_pc"], 16)
@ -111,7 +111,7 @@ def main():
print(f"cannot prepare public {args.suite} test: {error}", file=sys.stderr) print(f"cannot prepare public {args.suite} test: {error}", file=sys.stderr)
return 1 return 1
print(f"Prepared pinned public {args.suite} suite: {manifest}") print(f"Prepared pinned public {args.suite} suite: {manifest}")
print("Run with gameboy-rust --test-suite PATH. Preparation does not mean the CPU tests passed.") print("Run with gameboy-rust --test-suite PATH. Preparation does not mean the tests passed.")
return 0 return 0

View file

@ -23,9 +23,12 @@ class PreparationTests(unittest.TestCase):
if self.suite == "arm": if self.suite == "arm":
data = struct.pack("<II", 0xEAFFFFFE, 0xE92D0003) data = struct.pack("<II", 0xEAFFFFFE, 0xE92D0003)
pc, result = "0x08000004", 12 pc, result = "0x08000004", 12
else: elif self.suite == "thumb":
data = struct.pack("<IIHHI", 0xEAFFFFFE, 0xE1A00000, 0xA000, 0x4700, 0xE92D0003) data = struct.pack("<IIHHI", 0xEAFFFFFE, 0xE1A00000, 0xA000, 0x4700, 0xE92D0003)
pc, result = "0x0800000c", 7 pc, result = "0x0800000c", 7
else:
data = struct.pack("<III", 0xEAFFFFFE, 0xE1A00000, 0xE92D0003)
pc, result = "0x08000008", 12
self.files = { self.files = {
self.rom: data, self.rom: data,
"LICENSE": b"Original fixture, not upstream license content.\n", "LICENSE": b"Original fixture, not upstream license content.\n",
@ -59,7 +62,7 @@ class PreparationTests(unittest.TestCase):
return self.files[url[len(prefix):]] return self.files[url[len(prefix):]]
def prepare(self): def prepare(self):
# Exercise the existing default ARM API as well as explicit Thumb selection. # Exercise default ARM preparation and explicit selection of the other suites.
options = {} if self.suite == "arm" else {"suite": self.suite} options = {} if self.suite == "arm" else {"suite": self.suite}
return adapter.prepare(self.output, download=self.download, **options) return adapter.prepare(self.output, download=self.download, **options)
@ -185,6 +188,10 @@ class ThumbPreparationTests(PreparationTests):
self.assertFalse(self.output.exists()) self.assertFalse(self.output.exists())
class MemoryPreparationTests(PreparationTests):
suite = "memory"
class SuiteSelectionTests(unittest.TestCase): class SuiteSelectionTests(unittest.TestCase):
def test_unknown_suite_is_rejected_before_download_or_output(self): def test_unknown_suite_is_rejected_before_download_or_output(self):
with tempfile.TemporaryDirectory() as temp: with tempfile.TemporaryDirectory() as temp:
@ -199,7 +206,11 @@ class SuiteSelectionTests(unittest.TestCase):
self.assertFalse(output.exists()) self.assertFalse(output.exists())
def test_committed_locks_keep_distinct_verified_protocols(self): def test_committed_locks_keep_distinct_verified_protocols(self):
for suite, pc, register in [("arm", "0x08001d4c", 12), ("thumb", "0x08000934", 7)]: for suite, pc, register in [
("arm", "0x08001d4c", 12),
("thumb", "0x08000934", 7),
("memory", "0x08000350", 12),
]:
lock = json.loads(adapter.LOCKS[suite].read_text()) lock = json.loads(adapter.LOCKS[suite].read_text())
self.assertEqual(lock["repository"], "jsmolka/gba-tests") self.assertEqual(lock["repository"], "jsmolka/gba-tests")
self.assertEqual(lock["revision"], "a7113b67e63f83a9b321696ddd7042ccfad6c881") self.assertEqual(lock["revision"], "a7113b67e63f83a9b321696ddd7042ccfad6c881")