feat(bios): implement background and object affine services

Execute BgAffineSet and ObjAffineSet through original ARM routines with a mathematically generated sine table. Preserve caller state, validate record spans and strides, and use normal bus writes for background registers and sprite matrices.

Add independent matrix/origin references, all-phase coverage, rendering integration, diagnostics, stack and DMA checks. Extend image construction with multiple literal pools and reserved table bounds. Document formats and compatibility limits.

Validated on Darwin arm64: 694 default and 683 core-only tests in debug and release; formatting, Clippy, rustdoc, ten native window smoke tests, and CPU/timer demos pass.
This commit is contained in:
0xrsydn 2026-10-05 15:19:20 +07:00
commit f60db7e43b
5 changed files with 1020 additions and 23 deletions

View file

@ -253,7 +253,7 @@ Verified locally on macOS 15.7.3:
- Nix system: `aarch64-darwin`. - Nix system: `aarch64-darwin`.
- Rust host: `aarch64-apple-darwin`. - Rust host: `aarch64-apple-darwin`.
- Release executable: native Mach-O `arm64`, without Rosetta. - Release executable: native Mach-O `arm64`, without Rosetta.
- All 671 tests pass in debug and release builds. - All 694 tests pass in debug and release builds.
- All ten native window smoke tests pass. The mosaic test submits 128 frames; the other nine submit 60 frames each. - All ten native window smoke tests pass. The mosaic test submits 128 frames; the other nine submit 60 frames each.
- The eight CPU-driven smoke tests check CPU state, every output pixel, and presentation at VBlank entry. - The eight CPU-driven smoke tests check CPU state, every output pixel, and presentation at VBlank entry.
- The terminal-only CPU and timer IRQ demos run successfully. - The terminal-only CPU and timer IRQ demos run successfully.
@ -358,7 +358,7 @@ nix develop -c cargo test
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
- Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing.
- Optional caller-supplied 16 KiB BIOS mapping for vector code, without CPU BIOS read protection. - Optional caller-supplied 16 KiB BIOS mapping for vector code, without CPU BIOS read protection.
- Optional original BIOS replacement: minimal boot, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters. - Optional original BIOS replacement: minimal boot, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
- All 16 ARM data-processing operations, with immediate and shifted-register operands. - All 16 ARM data-processing operations, with immediate and shifted-register operands.
- Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`. - Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`.
- Arithmetic operations: `SUB`, `RSB`, `ADD`, `ADC`, `SBC`, `RSC`, `CMP`, and `CMN`. - Arithmetic operations: `SUB`, `RSB`, `ADD`, `ADC`, `SBC`, `RSC`, `CMP`, and `CMN`.
@ -477,7 +477,11 @@ Neither constructor provides Nintendo BIOS services or initializes BIOS-managed
### Optional original BIOS replacement ### Optional original BIOS replacement
`src/bios.rs` builds a deterministic 16 KiB image from original ARM instructions. No Nintendo firmware bytes are included. `src/bios.rs` builds a deterministic 16 KiB image from original ARM instructions and mathematically generated data.
No Nintendo firmware bytes are included.
A compile-time integer calculation generates the 512-byte sine table at `0x3e00..0x3fff`.
The image builder checks that code and literal pools do not overlap this table.
Literal pools follow unconditional branches and stay within each load instruction's address range.
The CPU executes every instruction through the normal bus, timing, exception, HALT, and DMA paths. The CPU executes every instruction through the normal bus, timing, exception, HALT, and DMA paths.
There is no host-side interception of SWIs. There is no host-side interception of SWIs.
@ -513,6 +517,8 @@ Supported software interrupt services:
| `0x0a` | ArcTan2 | Signed fixed-point X/Y in r0/r1; return the unsigned direction angle in r0 | | `0x0a` | ArcTan2 | Signed fixed-point X/Y in r0/r1; return the unsigned direction angle in r0 |
| `0x0b` | CpuSet | `r0`: source; `r1`: destination; `r2`: count, fill, and width control | | `0x0b` | CpuSet | `r0`: source; `r1`: destination; `r2`: count, fill, and width control |
| `0x0c` | CpuFastSet | Word copy/fill in eight-word blocks; round count upward to a multiple of eight | | `0x0c` | CpuFastSet | Word copy/fill in eight-word blocks; round count upward to a multiple of eight |
| `0x0e` | BgAffineSet | Build background matrices and origins from r0 into r1; r2 is the record count |
| `0x0f` | ObjAffineSet | Build sprite matrices from r0 into r1; r2 is the count, r3 the coefficient stride |
| `0x10` | BitUnPack | Expand packed units from r0 to r1 using the descriptor at r2; write complete words | | `0x10` | BitUnPack | Expand packed units from r0 to r1 using the descriptor at r2; write complete words |
| `0x11` | LZ77UnCompWram | Decompress from r0 to r1 with byte writes | | `0x11` | LZ77UnCompWram | Decompress from r0 to r1 with byte writes |
| `0x12` | LZ77UnCompVram | Decompress from r0 to r1 with buffered halfword writes | | `0x12` | LZ77UnCompVram | Decompress from r0 to r1 with buffered halfword writes |
@ -528,7 +534,7 @@ Only User/System callers are supported. Calls from exception modes return a diag
Services preserve CPSR and every caller register except the documented arithmetic outputs. Services preserve CPSR and every caller register except the documented arithmetic outputs.
This is deterministic behavior, not a claim about undocumented firmware outputs. This is deterministic behavior, not a claim about undocumented firmware outputs.
Supervisor stack use is 28 bytes normally, including ArcTan. Division and ArcTan2 use 40 bytes. Supervisor stack use is 28 bytes normally, including ArcTan. Division and ArcTan2 use 40 bytes.
CpuFastSet, BitUnPack, all three decompression formats, and differential filters use 60 bytes. CpuFastSet, affine-matrix services, BitUnPack, all three decompression formats, and differential filters use 60 bytes.
They do not reproduce the original firmware's internal stack layout or cycle counts. They do not reproduce the original firmware's internal stack layout or cycle counts.
**IRQ callback contract:** install a word-aligned ARM callback address at `0x03007ffc` after boot. **IRQ callback contract:** install a word-aligned ARM callback address at `0x03007ffc` after boot.
@ -593,6 +599,56 @@ Both services preserve r1–r14 and the caller's status. Undocumented BIOS scrat
They execute original ARM instructions and keep CPU IRQ delivery masked until return, without changing IME. They execute original ARM instructions and keep CPU IRQ delivery masked until return, without changing IME.
Device clocks and DMA continue. Exact firmware timing and full hardware compatibility remain unverified. Device clocks and DMA continue. Exact firmware timing and full hardware compatibility remain unverified.
#### Affine matrices
Both services take a source pointer in r0, destination pointer in r1, and unsigned record count in r2.
They generate inverse-mapping matrices for background and sprite rotation/scaling.
Scale values are signed 8.8: 256 represents 1.0. The services do not calculate reciprocal scales.
Angles use 65,536 units per turn, but only the upper byte is used.
`BgAffineSet` reads 20-byte, word-aligned source records:
| Offset | Field |
| --- | --- |
| 0, 4 | Signed 32-bit texture-center X/Y, with eight fractional bits |
| 8, 10 | Signed 16-bit display-center X/Y, in pixels |
| 12, 14 | Signed 16-bit X/Y scale |
| 16 | Unsigned 16-bit angle |
| 18 | Two padding bytes; not read |
Each word-aligned output record occupies 16 bytes.
It contains four signed halfwords, PA/PB/PC/PD, followed by two 32-bit background origins.
The destination can point directly to BG2 or BG3 affine registers.
`ObjAffineSet` reads eight-byte, halfword-aligned source records.
Offsets 0 and 2 contain signed X/Y scales; offset 4 contains the angle.
The final two padding bytes are not read.
The destination must be halfword-aligned. Register r3 specifies an even coefficient stride of at least two bytes.
Each matrix writes PA/PB/PC/PD at destination offsets `0*stride` through `3*stride`.
The next matrix starts at `4*stride`. Stride two packs coefficients; stride eight preserves intervening sprite attributes in OAM.
Other valid even strides work, and the services leave gaps unchanged.
The shared kernel reads signed sine/cosine values with 14 fractional bits from the generated table.
Products use arithmetic right shifts by 14, then narrow to signed 16-bit intermediates.
PB negates the narrowed X-scale sine product after rounding, not before the shift.
Background origins use those narrowed intermediates and wrap modulo 2³².
All signed scales are accepted, including zero and -32768.
Extreme overflow behavior follows the integer emulator reference; hardware equivalence remains unverified.
Zero count returns before buffer or stride validation, without source or output accesses.
For nonzero counts, invalid alignment, protected sources below `0x02000000`, or wrapping ranges reach `bios::INVALID_ARGUMENT_TRAP`.
Counts cover complete record spans, including padding and the trailing object-stride gap.
Overflow checks run before output writes. They do not require every source byte to be mapped in advance.
Use separate source and output buffers; overlapping buffers are not supported.
The services read each record's fields before writing its output.
A failed source read preserves earlier records without writing the current record.
A failed destination write preserves earlier successful stores, including stores within the current record.
Normal halfword/word bus rules apply to video memory and display registers.
Both services preserve all caller registers and status.
They keep CPU IRQ delivery masked without changing IME; device clocks and DMA continue.
Exact firmware cycle counts and undocumented side effects are not reproduced.
#### LZ77 decompression #### LZ77 decompression
Both variants read a word-aligned header at r0. Its low byte must be `0x10`; the upper 24 bits specify output length. Both variants read a word-aligned header at r0. Its low byte must be `0x10`; the upper 24 bits specify output length.
@ -752,7 +808,7 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op. The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction. Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction.
Prior boot/service steps remain committed on failure. Prior boot/service steps remain committed on failure.
BIOS affine-matrix helpers, reset services, and STOP remain unimplemented. BIOS reset services and STOP remain unimplemented.
This subset is not sufficient for Pokémon Emerald compatibility. This subset is not sufficient for Pokémon Emerald compatibility.
### Timers, interrupt registers, and the machine clock ### Timers, interrupt registers, and the machine clock
@ -1504,6 +1560,7 @@ Important timing limits:
| `src/lib.rs` | Core modules and original demo bytes | | `src/lib.rs` | Core modules and original demo bytes |
| `src/bios.rs` | Original ARM BIOS image builder, minimal boot, IRQ dispatch, waits, and memory services | | `src/bios.rs` | Original ARM BIOS image builder, minimal boot, IRQ dispatch, waits, and memory services |
| `src/bios/arithmetic.rs` | Emitted ARM division and integer-square-root routines | | `src/bios/arithmetic.rs` | Emitted ARM division and integer-square-root routines |
| `src/bios/affine.rs` | Emitted ARM background/sprite matrix services, range checks, and generated sine table |
| `src/bios/angles.rs` | Emitted ARM ArcTan polynomial, ArcTan2 ratio/quadrant handling, and input validation | | `src/bios/angles.rs` | Emitted ARM ArcTan polynomial, ArcTan2 ratio/quadrant handling, and input validation |
| `src/bios/lz77.rs` | Emitted ARM LZ77 decoder with byte and halfword output | | `src/bios/lz77.rs` | Emitted ARM LZ77 decoder with byte and halfword output |
| `src/bios/run_length.rs` | Emitted ARM run-length decoder, header validation, and block bounds | | `src/bios/run_length.rs` | Emitted ARM run-length decoder, header validation, and block bounds |
@ -1563,6 +1620,7 @@ Important timing limits:
| `tests/halt.rs` | HALT wake masks, idle timing, BIOS-only writes, DMA progress, and STOP diagnostics | | `tests/halt.rs` | HALT wake masks, idle timing, BIOS-only writes, DMA progress, and STOP diagnostics |
| `tests/bios.rs` | ARM/Thumb service calls, copy/fill boundaries, wait races, callback contracts, and boot | | `tests/bios.rs` | ARM/Thumb service calls, copy/fill boundaries, wait races, callback contracts, and boot |
| `tests/bios/arithmetic.rs` | Arithmetic boundaries, wide-integer references, status restoration, and zero-division diagnostics | | `tests/bios/arithmetic.rs` | Arithmetic boundaries, wide-integer references, status restoration, and zero-division diagnostics |
| `tests/bios/affine.rs` | Matrix/origin references, all angle phases, strides, live rendering, register preservation, and diagnostics |
| `tests/bios/angles.rs` | Fixed-point references, axes/quadrants, rounding, caller flags, stack bounds, DMA, and diagnostics | | `tests/bios/angles.rs` | Fixed-point references, axes/quadrants, rounding, caller flags, stack bounds, DMA, and diagnostics |
| `tests/bios/lz77.rs` | Token-reference tests, overlaps, output widths, malformed streams, and partial failures | | `tests/bios/lz77.rs` | Token-reference tests, overlaps, output widths, malformed streams, and partial failures |
| `tests/bios/run_length.rs` | All block controls, mixed streams, output widths, status, diagnostics, DMA, and IRQ masking | | `tests/bios/run_length.rs` | All block controls, mixed streams, output widths, status, diagnostics, DMA, and IRQ masking |
@ -1636,12 +1694,12 @@ To test the core without building the window dependency:
direnv exec . cargo test --locked --no-default-features direnv exec . cargo test --locked --no-default-features
``` ```
This runs 660 core and integration tests; the eleven desktop and command-line tests are excluded. This runs 683 core and integration tests; the eleven desktop and command-line tests are excluded.
On Apple Silicon, `file` must report a Mach-O `arm64` executable. On Apple Silicon, `file` must report a Mach-O `arm64` executable.
Do not set a Linux cross-compilation target for this validation. Do not set a Linux cross-compilation target for this validation.
The default suite has 671 tests. The default suite has 694 tests.
Angle-service tests compare results with wide-integer polynomial and signed-sector references, plus floating-point accuracy checks in supported ranges. Angle-service tests compare results with wide-integer polynomial and signed-sector references, plus floating-point accuracy checks in supported ranges.
They cover a dense ArcTan unit-interval grid, sampled full-domain and seeded inputs, axes, quadrant boundaries, signed extremes, and scale invariance. They cover a dense ArcTan unit-interval grid, sampled full-domain and seeded inputs, axes, quadrant boundaries, signed extremes, and scale invariance.
Other tests verify all caller flag combinations, User/System masks, non-result registers, stack bounds, repeated calls, diagnostics, and DMA/IRQ progress. Other tests verify all caller flag combinations, User/System masks, non-result registers, stack bounds, repeated calls, diagnostics, and DMA/IRQ progress.
@ -1716,6 +1774,11 @@ The CPU demo test checks sprite controls, OAM attributes, disabled unused object
Tile tests cover both color depths, palette banks, flips, all map sizes, scrolling, transparency, priorities, and register masks. Tile tests cover both color depths, palette banks, flips, all map sizes, scrolling, transparency, priorities, and register masks.
They check errors for unsupported features and out-of-range fetches without changing the output image. They check errors for unsupported features and out-of-range fetches without changing the output image.
The CPU tile demo test verifies asset copies, every pixel, wrapping, opposite directions, reset, and VBlank synchronization. The CPU tile demo test verifies asset copies, every pixel, wrapping, opposite directions, reset, and VBlank synchronization.
BIOS affine tests check all 256 angle phases against independent trigonometric and wide-integer references.
They cover signed scales, origin wraparound, rounding, ignored angle bits, strides, alignment, and partial failures.
Live rendering tests apply generated matrices to background registers and sprite attribute memory.
Other checks cover ARM/Thumb calls, register/status preservation, stack bounds, DMA, timers, and deferred IRQ delivery.
Image-builder tests verify multiple literal pools, branch fixups, and reserved table boundaries.
BIOS arithmetic tests cover signed boundaries, division overflow/zero, all sign combinations, and seeded integer inputs. BIOS arithmetic tests cover signed boundaries, division overflow/zero, all sign combinations, and seeded integer inputs.
Square-root tests check exact integer bounds around perfect squares and across unsigned inputs. Square-root tests check exact integer bounds around perfect squares and across unsigned inputs.
LZ77 tests compare emitted-code execution against independent token expansion, including 4,096-byte distances and overlapping runs. LZ77 tests compare emitted-code execution against independent token expansion, including 4,096-byte distances and overlapping runs.
@ -1772,7 +1835,7 @@ These tests do not replace validation against GBA hardware or public hardware te
1. Validate CPU behavior with public ARM7TDMI test programs before claiming instruction compatibility. 1. Validate CPU behavior with public ARM7TDMI test programs before claiming instruction compatibility.
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. Expand BIOS services with affine-matrix helpers and reset functions; add STOP and remaining DMA device modes. 3. Expand BIOS services with reset functions; add STOP and remaining DMA device modes.
4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition. 4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition.
5. Expand graphics, audio, cartridge loading, and saves before testing Emerald compatibility. 5. Expand graphics, audio, cartridge loading, and saves before testing Emerald compatibility.
@ -1786,6 +1849,8 @@ Hardware references used for the core:
- [GBATEK BIOS halt functions](https://problemkaputt.de/gbatek-bios-halt-functions.htm), for wait contracts and STOP differences. - [GBATEK BIOS halt functions](https://problemkaputt.de/gbatek-bios-halt-functions.htm), for wait contracts and STOP differences.
- [GBATEK BIOS function calling conventions](https://problemkaputt.de/gbatek-bios-functions.htm). - [GBATEK BIOS function calling conventions](https://problemkaputt.de/gbatek-bios-functions.htm).
- [GBATEK BIOS memory-copy services](https://problemkaputt.de/gbatek-bios-memory-copy.htm). - [GBATEK BIOS memory-copy services](https://problemkaputt.de/gbatek-bios-memory-copy.htm).
- [GBATEK BIOS rotation/scaling services](https://problemkaputt.de/gbatek-bios-rotation-scaling-functions.htm), for record layouts, angle units, and output strides.
- [VisualBoyAdvance-M BIOS services](https://github.com/visualboyadvance-m/visualboyadvance-m/blob/master/src/core/gba/internal/gbaBios.cpp), reviewed for integer affine rounding and signed matrix intermediates. Our sine table is generated mathematically.
- [GBATEK BIOS arithmetic services](https://problemkaputt.de/gbatek-bios-arithmetic-functions.htm). - [GBATEK BIOS arithmetic services](https://problemkaputt.de/gbatek-bios-arithmetic-functions.htm).
- [GBATEK BIOS decompression services](https://problemkaputt.de/gbatek-bios-decompression-functions.htm), including BitUnPack descriptor fields, differential-filter headers, Huffman tree layout, and width constraints. - [GBATEK BIOS decompression services](https://problemkaputt.de/gbatek-bios-decompression-functions.htm), including BitUnPack descriptor fields, differential-filter headers, Huffman tree layout, and width constraints.
- [mGBA BIOS service implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c), reviewed for angle-polynomial coefficients and quadrant conventions, BitUnPack ordering/offsets, and Huffman tree layout and packing. - [mGBA BIOS service implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c), reviewed for angle-polynomial coefficients and quadrant conventions, BitUnPack ordering/offsets, and Huffman tree layout and packing.

View file

@ -1,10 +1,11 @@
//! Original, optional ARM BIOS replacement. No Nintendo firmware is included. //! Original, optional ARM BIOS replacement. No Nintendo firmware is included.
//! Supports interrupt waits, integer/fixed-point arithmetic, memory copy/fill, bit unpacking, //! Supports interrupt waits, integer/fixed-point arithmetic, affine matrices,
//! LZ77/run-length/Huffman decoding, and differential filters. //! memory copy/fill, bit unpacking, LZ77/run-length/Huffman decoding, and differential filters.
//! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. //! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS.
use std::collections::BTreeMap; use std::collections::BTreeMap;
mod affine;
mod angles; mod angles;
mod arithmetic; mod arithmetic;
mod bit_unpack; mod bit_unpack;
@ -42,7 +43,8 @@ pub fn boot(rom: Vec<u8>) -> Result<Machine, MemoryError> {
)) ))
} }
/// Build a deterministic 16 KiB image from original ARM instructions. /// Build a deterministic 16 KiB image from original ARM instructions and a
/// mathematically generated sine table. No Nintendo firmware bytes are included.
/// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte. /// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte.
/// Services support User/System callers and preserve caller status. Registers /// Services support User/System callers and preserve caller status. Registers
/// other than each arithmetic service's documented outputs are preserved. /// other than each arithmetic service's documented outputs are preserved.
@ -126,6 +128,8 @@ pub fn image() -> Vec<u8> {
(0x0a, "arctan2"), (0x0a, "arctan2"),
(0x0b, "cpu_set"), (0x0b, "cpu_set"),
(0x0c, "cpu_fast_set"), (0x0c, "cpu_fast_set"),
(0x0e, "bg_affine"),
(0x0f, "obj_affine"),
(0x10, "bit_unpack"), (0x10, "bit_unpack"),
(0x11, "lz_wram"), (0x11, "lz_wram"),
(0x12, "lz_vram"), (0x12, "lz_vram"),
@ -226,7 +230,11 @@ pub fn image() -> Vec<u8> {
["copy_halfword", "half_loop", "half_fill", "half_fill_loop"], ["copy_halfword", "half_loop", "half_fill", "half_fill_loop"],
); );
// The preceding routine ends with an unconditional branch. Place a literal
// pool here so reset/dispatcher loads stay within ARM's 4 KiB reach.
a.flush_literals();
arithmetic::emit(&mut a); arithmetic::emit(&mut a);
affine::emit(&mut a);
angles::emit(&mut a); angles::emit(&mut a);
bit_unpack::emit(&mut a); bit_unpack::emit(&mut a);
lz77::emit(&mut a); lz77::emit(&mut a);
@ -247,7 +255,9 @@ pub fn image() -> Vec<u8> {
a.emit(UNSUPPORTED_TRAP); a.emit(UNSUPPORTED_TRAP);
a.label("invalid_argument"); a.label("invalid_argument");
a.emit(INVALID_ARGUMENT_TRAP); a.emit(INVALID_ARGUMENT_TRAP);
a.finish() let mut image = a.finish(affine::TABLE_ADDRESS as usize);
affine::write_table(&mut image);
image
} }
fn emit_count(a: &mut ArmImage) { fn emit_count(a: &mut ArmImage) {
@ -327,7 +337,20 @@ impl ArmImage {
self.emit(0); self.emit(0);
} }
fn finish(mut self) -> Vec<u8> { // Call only after an unconditional branch/return/trap, never on a fallthrough path.
fn flush_literals(&mut self) {
for (index, register, value) in std::mem::take(&mut self.literals) {
let offset = (self.code.len() as i32 - index as i32 - 2) * 4;
assert!(offset.unsigned_abs() <= 0xfff);
self.code[index] = (if offset < 0 { 0xe51f_0000 } else { 0xe59f_0000 })
| register << 12
| offset.unsigned_abs();
self.code.push(value);
}
}
fn finish(mut self, code_limit: usize) -> Vec<u8> {
self.flush_literals();
for (index, condition, target, link) in self.branches { for (index, condition, target, link) in self.branches {
let offset = self.labels[target] as i32 - index as i32 - 2; let offset = self.labels[target] as i32 - index as i32 - 2;
assert!((-0x80_0000..0x80_0000).contains(&offset)); assert!((-0x80_0000..0x80_0000).contains(&offset));
@ -336,17 +359,54 @@ impl ArmImage {
| (u32::from(link) << 24) | (u32::from(link) << 24)
| (offset as u32 & 0x00ff_ffff); | (offset as u32 & 0x00ff_ffff);
} }
for (index, register, value) in self.literals {
let offset = (self.code.len() as i32 - index as i32 - 2) * 4;
assert!(offset.unsigned_abs() <= 0xfff);
self.code[index] = (if offset < 0 { 0xe51f_0000 } else { 0xe59f_0000 })
| register << 12
| offset.unsigned_abs();
self.code.push(value);
}
let mut bytes: Vec<u8> = self.code.into_iter().flat_map(u32::to_le_bytes).collect(); let mut bytes: Vec<u8> = self.code.into_iter().flat_map(u32::to_le_bytes).collect();
assert!(bytes.len() <= BIOS_SIZE); assert!(
bytes.len() <= code_limit,
"BIOS code overlaps reserved data"
);
assert!(code_limit <= BIOS_SIZE);
bytes.resize(BIOS_SIZE, 0); bytes.resize(BIOS_SIZE, 0);
bytes bytes
} }
} }
#[cfg(test)]
mod image_tests {
use super::*;
#[test]
fn multiple_literal_pools_preserve_pc_offsets_and_branch_fixups() {
let mut a = ArmImage::default();
a.literal(0, 0x1122_3344);
a.branch(14, "after_pool");
a.flush_literals();
a.label("after_pool");
a.literal(1, 0x5566_7788);
a.emit(0xe12f_ff1e); // BX lr
let image = a.finish(BIOS_SIZE);
let actual: Vec<_> = image[..24]
.chunks_exact(4)
.map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
.collect();
assert_eq!(
actual,
[
0xe59f_0000,
0xea00_0000,
0x1122_3344,
0xe59f_1000,
0xe12f_ff1e,
0x5566_7788
]
);
}
#[test]
#[should_panic(expected = "BIOS code overlaps reserved data")]
fn code_cannot_overlap_reserved_tables() {
let mut a = ArmImage::default();
a.emit(0xe1a0_0000);
a.emit(0xe1a0_0000);
a.finish(4);
}
}

186
src/bios/affine.rs Normal file
View file

@ -0,0 +1,186 @@
//! Original ARM affine-matrix services with a mathematically generated sine table.
//! Scaling uses signed 8.8 values and the upper angle byte. Matrix intermediates
//! narrow to signed halfwords before background-origin calculation.
use super::ArmImage;
pub(super) const TABLE_ADDRESS: u32 = 0x3e00;
const UNIT: i128 = 1i128 << 48;
// floor(pi * 2^48), not data extracted from a firmware image.
const PI: i128 = 884_279_719_003_555;
const fn quarter_sine(index: usize) -> i16 {
if index == 64 {
return 16384;
}
let x = PI * index as i128 / 128;
let mut term = x;
let mut sum = x;
let mut n = 1;
while n <= 10 {
term = -((term * x / UNIT) * x / UNIT) / ((2 * n) * (2 * n + 1));
sum += term;
n += 1;
}
(sum * 16384 / UNIT) as i16
}
const fn sine_table() -> [i16; 256] {
let mut table = [0; 256];
let mut index = 0;
while index < 256 {
let phase = index & 63;
table[index] = match index / 64 {
0 => quarter_sine(phase),
1 => quarter_sine(64 - phase),
2 => -quarter_sine(phase),
_ => -quarter_sine(64 - phase),
};
index += 1;
}
table
}
const SINE: [i16; 256] = sine_table();
pub(super) fn write_table(image: &mut [u8]) {
for (index, value) in SINE.iter().enumerate() {
let address = TABLE_ADDRESS as usize + index * 2;
image[address..address + 2].copy_from_slice(&value.to_le_bytes());
}
}
pub(super) fn emit(a: &mut ArmImage) {
a.label("bg_affine");
entry(a, true);
a.label("bg_affine_record");
a.emit(0xe1d0_40fc); // LDRSH r4,[r0,#12] (SX)
a.emit(0xe1d0_50fe); // LDRSH r5,[r0,#14] (SY)
a.emit(0xe1d0_61b0); // LDRH r6,[r0,#16] (angle)
a.call("affine_matrix");
a.emit(0xe590_8000); // LDR r8,[r0] (texture center X, signed 24.8)
a.emit(0xe590_9004); // LDR r9,[r0,#4] (texture center Y)
a.emit(0xe1d0_a0f8); // LDRSH r10,[r0,#8] (display center X)
a.emit(0xe1d0_b0fa); // LDRSH r11,[r0,#10] (display center Y)
a.emit(0xe00c_0a94); // MUL r12,r4,r10
a.emit(0xe048_800c); // SUB r8,r8,r12
a.emit(0xe00c_0b95); // MUL r12,r5,r11
a.emit(0xe088_800c); // ADD r8,r8,r12 (X = centerX - PA*displayX + SXsin*displayY)
a.emit(0xe00c_0a96); // MUL r12,r6,r10
a.emit(0xe049_900c); // SUB r9,r9,r12
a.emit(0xe00c_0b97); // MUL r12,r7,r11
a.emit(0xe049_900c); // SUB r9,r9,r12 (Y = centerY - PC*displayX - PD*displayY)
a.emit(0xe265_5000); // RSB r5,r5,#0 (PB: negate after signed shift/narrowing)
a.emit(0xe1c1_40b0); // STRH r4,[r1]
a.emit(0xe1c1_50b2); // STRH r5,[r1,#2]
a.emit(0xe1c1_60b4); // STRH r6,[r1,#4]
a.emit(0xe1c1_70b6); // STRH r7,[r1,#6]
a.emit(0xe581_8008); // STR r8,[r1,#8]
a.emit(0xe581_900c); // STR r9,[r1,#12]
a.emit(0xe280_0014); // ADD r0,r0,#20 (skip trailing two-byte source padding)
a.emit(0xe281_1010); // ADD r1,r1,#16
a.emit(0xe252_2001); // SUBS r2,r2,#1
a.branch(1, "bg_affine_record");
a.branch(14, "affine_done");
a.label("obj_affine");
entry(a, false);
a.label("obj_affine_record");
a.emit(0xe1d0_40f0); // LDRSH r4,[r0] (SX)
a.emit(0xe1d0_50f2); // LDRSH r5,[r0,#2] (SY)
a.emit(0xe1d0_60b4); // LDRH r6,[r0,#4] (angle)
a.call("affine_matrix");
a.emit(0xe265_5000); // RSB r5,r5,#0 (PB)
for register in 4..=7 {
a.emit(0xe1c1_00b0 | register << 12); // STRH rN,[r1]
a.emit(0xe081_1003); // ADD r1,r1,r3 (caller-supplied coefficient stride)
}
a.emit(0xe280_0008); // ADD r0,r0,#8 (skip trailing two-byte padding)
a.emit(0xe252_2001); // SUBS r2,r2,#1
a.branch(1, "obj_affine_record");
a.label("affine_done");
a.emit(0xe8bd_0ff0); // LDMIA sp!,{r4-r11}
a.branch(14, "return");
// Leaf kernel preserves argument pointers/count/stride. r4-r7 return PA,
// pre-negation SXsin, PC, PD. r8-r11 are scratch; no additional stack use.
a.label("affine_matrix");
a.emit(0xe1a0_6426); // MOV r6,r6,LSR #8 (discard fractional angle byte)
a.literal(10, TABLE_ADDRESS);
a.emit(0xe08a_b086); // ADD r11,r10,r6,LSL #1
a.emit(0xe1db_80f0); // LDRSH r8,[r11] (sine in signed 2.14)
a.emit(0xe286_6040); // ADD r6,r6,#64
a.emit(0xe206_60ff); // AND r6,r6,#255
a.emit(0xe08a_b086); // ADD r11,r10,r6,LSL #1
a.emit(0xe1db_90f0); // LDRSH r9,[r11] (cosine)
a.emit(0xe006_0895); // MUL r6,r5,r8 (SYsin)
a.emit(0xe007_0995); // MUL r7,r5,r9 (SYcos)
a.emit(0xe005_0894); // MUL r5,r4,r8 (SXsin)
a.emit(0xe008_0994); // MUL r8,r4,r9 (SXcos)
a.emit(0xe1a0_4748); // MOV r4,r8,ASR #14
for register in 5..=7 {
a.emit(0xe1a0_0740 | register << 12 | register); // MOV rN,rN,ASR #14
}
for register in 4..=7 {
a.emit(0xe1a0_0800 | register << 12 | register); // MOV rN,rN,LSL #16
a.emit(0xe1a0_0840 | register << 12 | register); // MOV rN,rN,ASR #16
}
a.emit(0xe12f_ff1e); // BX lr
}
fn entry(a: &mut ArmImage, background: bool) {
a.emit(0xe352_0000); // CMP r2,#0
a.branch(0, "return"); // Zero count does not validate or access buffers.
if !background {
// SWI dispatch uses r3 for caller status. Recover the fourth argument
// from the common frame before adding our local register save.
a.emit(0xe59d_3010); // LDR r3,[sp,#16] (original coefficient stride)
}
a.emit(0xe92d_0ff0); // STMDB sp!,{r4-r11} (60 bytes total SVC stack use)
a.emit(0xe180_c001); // ORR r12,r0,r1
a.emit(if background { 0xe31c_0003 } else { 0xe31c_0001 }); // TST alignment
a.branch(1, "invalid_argument");
a.emit(0xe350_0402); // CMP r0,#0x02000000
a.branch(3, "invalid_argument");
if !background {
a.emit(0xe313_0001); // TST r3,#1 (even coefficient stride)
a.branch(1, "invalid_argument");
a.emit(0xe353_0002); // CMP r3,#2
a.branch(3, "invalid_argument");
a.emit(0xe313_0103); // TST r3,#0xc0000000 (4*stride must not wrap)
a.branch(1, "invalid_argument");
}
a.emit(if background { 0xe3a0_6014 } else { 0xe3a0_6008 }); // MOV r6,#source stride
check_span(a, 0);
a.emit(if background { 0xe3a0_6010 } else { 0xe1a0_6103 }); // MOV r6,#16 / r3,LSL #2
check_span(a, 1);
}
fn check_span(a: &mut ArmImage, pointer: u32) {
a.emit(0xe085_4692); // UMULL r4,r5,r2,r6 (full unsigned count*stride)
a.emit(0xe355_0000); // CMP r5,#0
a.branch(1, "invalid_argument");
a.emit(0xe090_4004 | pointer << 16); // ADDS r4,rPointer,r4
a.branch(2, "invalid_argument");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_table_matches_independent_trigonometry_for_every_phase() {
for (phase, actual) in SINE.iter().enumerate() {
let angle = phase as f64 * std::f64::consts::TAU / 256.0;
assert_eq!(
*actual,
(angle.sin() * 16384.0).trunc() as i16,
"phase={phase}"
);
}
assert_eq!(
[SINE[0], SINE[64], SINE[128], SINE[192]],
[0, 16384, 0, -16384]
);
}
}

View file

@ -12,6 +12,8 @@ const DEST: u32 = 0x0201_0000;
const CALLBACK: u32 = 0x0300_0100; const CALLBACK: u32 = 0x0300_0100;
const ROM_DATA: u32 = ROM_START + 13 * 4; const ROM_DATA: u32 = ROM_START + 13 * 4;
#[path = "bios/affine.rs"]
mod affine;
#[path = "bios/angles.rs"] #[path = "bios/angles.rs"]
mod angles; mod angles;
#[path = "bios/arithmetic.rs"] #[path = "bios/arithmetic.rs"]
@ -225,7 +227,7 @@ fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() {
#[test] #[test]
fn unsupported_services_fail_at_an_explicit_instruction_trap() { fn unsupported_services_fail_at_an_explicit_instruction_trap() {
for service in [0, 1, 3, 0x0d, 0x0e, 0x0f, 0x19, 0xff] { for service in [0, 1, 3, 0x0d, 0x19, 0xff] {
let (mut machine, _) = call(service, false, [0, 0, 0]); let (mut machine, _) = call(service, false, [0, 0, 0]);
let error = (0..100) let error = (0..100)
.find_map(|_| machine.step().err()) .find_map(|_| machine.step().err())

684
tests/bios/affine.rs Normal file
View file

@ -0,0 +1,684 @@
use super::*;
use gba_rust::{
bios::INVALID_ARGUMENT_TRAP,
dma::DMA_BASE,
io::{BG2PA, DISPCNT},
memory::{OAM_START as OAM, PALETTE_START as PAL},
video::{rgb555_to_rgb888 as rgb, Framebuffer},
};
const DATA: u32 = ROM_START + 23 * 4;
#[derive(Clone, Copy)]
struct Bg {
texture: [i32; 2],
screen: [i16; 2],
scale: [i16; 2],
angle: u16,
}
#[derive(Clone, Copy)]
struct Obj {
scale: [i16; 2],
angle: u16,
}
fn bg_bytes(records: &[Bg]) -> Vec<u8> {
records
.iter()
.flat_map(|r| {
let mut bytes = Vec::new();
for v in r.texture {
bytes.extend(v.to_le_bytes());
}
for v in r.screen.into_iter().chain(r.scale) {
bytes.extend(v.to_le_bytes());
}
bytes.extend(r.angle.to_le_bytes());
bytes.extend([0xa5, 0x5a]); // Ignored structure padding.
bytes
})
.collect()
}
fn obj_bytes(records: &[Obj]) -> Vec<u8> {
records
.iter()
.flat_map(|r| {
let mut bytes = Vec::new();
for v in r.scale {
bytes.extend(v.to_le_bytes());
}
bytes.extend(r.angle.to_le_bytes());
bytes.extend([0xa5, 0x5a]);
bytes
})
.collect()
}
// Four-argument call harness. Seed r4-r12 to check nonzero register preservation.
fn prepare(service: u8, thumb: bool, args: [u32; 4], status: u32, data: &[u8]) -> (Machine, u32) {
let mut code: Vec<u32> = (0..4).map(|r| 0xe59f_0044 | r << 12).collect();
for r in 4..=12 {
code.push(0xe3a0_0040 | r << 12 | r);
}
code.extend([
0xe321_f000 | status,
0xe328_f480, // MSR CPSR_f,#N
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
if thumb {
0xe7fe_df00 | u32::from(service)
} else {
0xef00_0000 | u32::from(service) << 16
},
0xeaff_fffe,
]);
code.extend(args);
let mut rom = words(&code);
assert_eq!(rom.len() as u32 + ROM_START, DATA);
rom.extend(data);
let mut m = bios::boot(rom).unwrap();
reach(&mut m, ROM_START + 68, 100);
(m, ROM_START + if thumb { 70 } else { 72 })
}
fn complete(m: &mut Machine, pc: u32) {
let before = m.cpu().clone();
m.memory_mut()
.write32(bios::IRQ_STACK, 0x1234_5678)
.unwrap();
m.step().unwrap();
reach(m, pc, 3_000_000);
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
assert_eq!(m.cpu().cpsr(), before.cpsr());
assert_eq!(m.memory().read32(bios::IRQ_STACK).unwrap(), 0x1234_5678);
}
// Independent floating-point table generation and wide-integer matrix arithmetic.
fn coefficients(scale: [i16; 2], angle: u16) -> [i16; 4] {
let phase = usize::from(angle >> 8);
let sine = |index: usize| {
((index as f64 * std::f64::consts::TAU / 256.0).sin() * 16384.0).trunc() as i64
};
let s = sine(phase);
let c = sine((phase + 64) % 256);
let convert = |scale: i16, v: i64| (i64::from(scale) * v).div_euclid(16384) as i16;
[
convert(scale[0], c),
convert(scale[0], s),
convert(scale[1], s),
convert(scale[1], c),
]
}
fn bg_reference(r: Bg) -> Vec<u8> {
let [pa, sxsin, pc, pd] = coefficients(r.scale, r.angle);
let x = i64::from(r.texture[0]) - i64::from(pa) * i64::from(r.screen[0])
+ i64::from(sxsin) * i64::from(r.screen[1]);
let y = i64::from(r.texture[1])
- i64::from(pc) * i64::from(r.screen[0])
- i64::from(pd) * i64::from(r.screen[1]);
let mut bytes: Vec<_> = [pa, sxsin.wrapping_neg(), pc, pd]
.into_iter()
.flat_map(i16::to_le_bytes)
.collect();
bytes.extend((x as u32).to_le_bytes());
bytes.extend((y as u32).to_le_bytes());
bytes
}
fn verify_bg(records: &[Bg], thumb: bool, dest: u32) {
let (mut m, pc) = prepare(
0x0e,
thumb,
[DATA, dest, records.len() as u32, 0xdead_beef],
0x1f,
&bg_bytes(records),
);
let end = dest + records.len() as u32 * 16;
for address in [dest - 4, end] {
m.memory_mut().write32(address, 0xa55a_1234).unwrap();
}
complete(&mut m, pc);
let expected: Vec<_> = records.iter().flat_map(|r| bg_reference(*r)).collect();
for (i, byte) in expected.iter().enumerate() {
assert_eq!(
m.memory().read8(dest + i as u32).unwrap(),
*byte,
"byte={i}"
);
}
for address in [dest - 4, end] {
assert_eq!(m.memory().read32(address).unwrap(), 0xa55a_1234);
}
}
fn verify_obj(records: &[Obj], thumb: bool, dest: u32, stride: u32) {
let data = obj_bytes(records);
let span = records.len() as u32 * stride * 4;
let (mut m, pc) = prepare(
0x0f,
thumb,
[DATA, dest, records.len() as u32, stride],
0x1f,
&data,
);
let mut expected = vec![0xa55au16; span as usize / 2];
for (i, r) in records.iter().enumerate() {
let [pa, sxsin, pc, pd] = coefficients(r.scale, r.angle);
for (j, value) in [pa, sxsin.wrapping_neg(), pc, pd].into_iter().enumerate() {
expected[(i * 4 + j) * stride as usize / 2] = value as u16;
}
}
for offset in (0..span).step_by(2) {
m.memory_mut().write16(dest + offset, 0xa55a).unwrap();
}
for address in [dest - 2, dest + span] {
m.memory_mut().write16(address, 0x1357).unwrap();
}
complete(&mut m, pc);
for (i, value) in expected.iter().enumerate() {
assert_eq!(
m.memory().read16(dest + i as u32 * 2).unwrap(),
*value,
"halfword={i}"
);
}
for address in [dest - 2, dest + span] {
assert_eq!(m.memory().read16(address).unwrap(), 0x1357);
}
}
fn fail(m: &mut Machine) -> MachineError {
for _ in 0..3000 {
let before = m.cpu().clone();
let cycles = m.cycles();
let timing = m.last_timing();
if let Err(error) = m.step() {
assert_eq!(m.cpu(), &before);
assert_eq!(m.cycles(), cycles);
assert_eq!(m.last_timing(), timing);
assert_eq!(m.step(), Err(error.clone()));
return error;
}
}
panic!("invalid affine call did not produce a bounded diagnostic");
}
fn assert_invalid(error: MachineError) {
assert!(matches!(
error,
MachineError::Cpu(CpuError::UnsupportedInstruction {
instruction: INVALID_ARGUMENT_TRAP,
..
})
));
}
#[test]
fn background_identity_translates_texture_and_display_centers() {
let r = Bg {
texture: [0x12345, -0x12345],
screen: [12, -34],
scale: [256, 256],
angle: 0,
};
let expected = bg_reference(r);
assert_eq!(&expected[..8], &[0, 1, 0, 0, 0, 0, 0, 1]);
for thumb in [false, true] {
verify_bg(&[r], thumb, DEST);
}
}
#[test]
fn object_cardinal_rotations_write_packed_and_oam_matrices() {
let records: Vec<_> = [0, 0x4000, 0x8000, 0xc000]
.into_iter()
.map(|angle| Obj {
scale: [256, 512],
angle,
})
.collect();
for thumb in [false, true] {
verify_obj(&records, thumb, DEST, 2);
verify_obj(&records, thumb, OAM + 6, 8);
}
}
#[test]
fn every_angle_phase_matches_independent_matrix_and_origin_references() {
let records: Vec<_> = (0..256u16)
.map(|phase| Bg {
texture: [0x123456, -0x234567],
screen: [-123, 234],
scale: [257, -513],
angle: phase << 8,
})
.collect();
verify_bg(&records, false, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, true, DEST, 8);
}
#[test]
fn low_angle_byte_and_record_padding_do_not_change_results() {
let records: Vec<_> = (0..256u16)
.map(|low| Bg {
texture: [-12345, 98765],
screen: [45, -67],
scale: [319, 711],
angle: 0x2300 | low,
})
.collect();
let expected = bg_reference(records[0]);
assert!(records.iter().all(|r| bg_reference(*r) == expected));
verify_bg(&records, true, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, false, DEST, 2);
}
#[test]
fn signed_scales_zero_reflections_and_halfword_wrap_match_reference() {
let mut records = Vec::new();
for sx in [-32768, -32767, -257, -1, 0, 1, 127, 256, 32767] {
for sy in [-32768, -1, 0, 1, 256, 32767] {
for angle in [0, 0x100, 0x4000, 0x8000, 0xc000, 0xff00] {
records.push(Bg {
texture: [i32::MIN, i32::MAX],
screen: [i16::MIN, i16::MAX],
scale: [sx, sy],
angle,
});
}
}
}
verify_bg(&records, false, DEST);
let objects: Vec<_> = records
.iter()
.map(|r| Obj {
scale: r.scale,
angle: r.angle,
})
.collect();
verify_obj(&objects, true, DEST, 2);
}
#[test]
fn pb_negation_occurs_after_signed_rounding() {
assert_eq!(coefficients([1, 1], 0x100), [0, 0, 0, 0]);
assert_eq!(coefficients([-1, -1], 0x100), [-1, -1, -1, -1]);
let records = [
Obj {
scale: [1, 1],
angle: 0x100,
},
Obj {
scale: [-1, -1],
angle: 0x100,
},
];
verify_obj(&records, false, DEST, 2); // PB must be 0 then +1, not -1 then 0.
}
#[test]
fn seeded_origins_and_matrices_match_wide_integer_wraparound() {
let mut state = 0x817b_53e9u32;
let mut next = || {
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
state
};
let records: Vec<_> = (0..512)
.map(|_| Bg {
texture: [next() as i32, next() as i32],
screen: [next() as i16, next() as i16],
scale: [next() as i16, next() as i16],
angle: next() as u16,
})
.collect();
verify_bg(&records, true, DEST);
}
#[test]
fn custom_even_strides_preserve_gaps_between_coefficients_and_records() {
let records = [
Obj {
scale: [256, 128],
angle: 0x1200,
},
Obj {
scale: [-257, 513],
angle: 0xa500,
},
Obj {
scale: [0, -1],
angle: 0xffff,
},
];
for stride in [2, 4, 6, 8, 16, 32, 1024] {
verify_obj(&records, false, DEST, stride);
}
}
#[test]
fn zero_count_skips_all_buffer_and_stride_validation() {
for service in [0x0e, 0x0f] {
let (mut m, pc) = prepare(service, true, [1, 0xffff_ffff, 0, 1], 0x1f, &[]);
complete(&mut m, pc);
}
}
#[test]
fn alignment_and_protected_sources_are_rejected_before_output() {
for service in [0x0e, 0x0f] {
for source in [0, 0x3ffc, 0x0100_0000, DATA + 1, DATA + 3] {
let (mut m, _) = prepare(service, false, [source, DEST, 1, 2], 0x1f, &[0; 20]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
let (mut m, _) = prepare(service, false, [DATA, DEST + 1, 1, 2], 0x1f, &[0; 20]);
assert_invalid(fail(&mut m));
}
for (source, dest) in [(DATA + 2, DEST), (DATA, DEST + 2)] {
let (mut m, _) = prepare(0x0e, false, [source, dest, 1, 2], 0x1f, &[0; 24]);
assert_invalid(fail(&mut m));
}
let mut data = vec![0, 0];
data.extend(obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}]));
let (mut m, pc) = prepare(0x0f, false, [DATA + 2, DEST + 2, 1, 2], 0x1f, &data);
complete(&mut m, pc); // OBJ only requires halfword alignment.
assert_eq!(m.memory().read16(DEST + 2).unwrap(), 256);
}
#[test]
fn invalid_stride_batch_sizes_and_wrapping_ranges_are_diagnostics() {
for stride in [0, 1, 3, 0x4000_0000, 0xffff_fffe] {
let (mut m, _) = prepare(0x0f, false, [DATA, DEST, 1, stride], 0x1f, &[]);
assert_invalid(fail(&mut m));
}
for service in [0x0e, 0x0f] {
for (source, dest, count, stride) in [
(DATA, DEST, u32::MAX, 2),
(0xffff_fff8, DEST, 1, 2),
(DATA, 0xffff_fff8, 1, 2),
(DATA, DEST, 0x1000_0000, 32),
] {
let (mut m, _) = prepare(service, false, [source, dest, count, stride], 0x1f, &[]);
assert_invalid(fail(&mut m));
assert_eq!(m.memory().read32(DEST).unwrap(), 0);
}
}
}
#[test]
fn last_record_padding_need_not_be_mapped() {
for service in [0x0e, 0x0f] {
let mut data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
data.truncate(data.len() - 2);
let (mut m, pc) = prepare(service, false, [DATA, DEST, 1, 2], 0x1f, &data);
complete(&mut m, pc);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
}
}
#[test]
fn later_source_failure_preserves_first_record_but_not_partial_new_output() {
for service in [0x0e, 0x0f] {
let mut data = if service == 0x0e {
bg_bytes(
&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}; 2],
)
} else {
obj_bytes(
&[Obj {
scale: [256, 256],
angle: 0,
}; 2],
)
};
data.truncate(data.len() - 3); // Missing last angle byte in second record.
let span = if service == 0x0e { 16 } else { 8 };
let (mut m, _) = prepare(service, false, [DATA, DEST, 2, 2], 0x1f, &data);
m.memory_mut().write32(DEST + span, 0xcccc_cccc).unwrap();
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::Unmapped(DATA + data.len() as u32).into())
);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.memory().read16(DEST + 6).unwrap(), 256);
assert_eq!(m.memory().read32(DEST + span).unwrap(), 0xcccc_cccc);
}
}
#[test]
fn destination_failure_keeps_preceding_halfword_stores() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
let (mut m, _) = prepare(service, false, [DATA, 0x07ff_fffc, 1, 2], 0x1f, &data);
assert_eq!(
fail(&mut m),
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
);
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 256);
}
}
#[test]
fn both_work_ram_regions_can_supply_records() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
for source in [SOURCE, 0x0300_0100] {
let (mut m, pc) = prepare(service, true, [source, DEST, 1, 2], 0x1f, &[]);
for (i, byte) in data.iter().enumerate() {
m.memory_mut().write8(source + i as u32, *byte).unwrap();
}
complete(&mut m, pc);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.memory().read16(DEST + 6).unwrap(), 256);
}
}
}
#[test]
fn user_system_status_masks_and_nonzero_caller_registers_are_preserved() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [-12345, 67890],
screen: [-123, 321],
scale: [-257, 519],
angle: 0xa501,
}])
} else {
obj_bytes(&[Obj {
scale: [-257, 519],
angle: 0xa501,
}])
};
for thumb in [false, true] {
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
let (mut m, pc) = prepare(service, thumb, [DATA, DEST, 1, 8], status, &data);
complete(&mut m, pc);
}
}
}
}
#[test]
fn background_service_can_write_live_affine_registers_and_render() {
let data = bg_bytes(&[Bg {
texture: [256, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}]);
let (mut m, pc) = prepare(0x0e, false, [DATA, BG2PA, 1, 0], 0x1f, &data);
m.memory_mut().write16(DISPCNT, 0x0403).unwrap();
m.memory_mut().write16(VRAM_START, 31).unwrap();
m.memory_mut().write16(VRAM_START + 2, 0x3e0).unwrap();
complete(&mut m, pc);
// Affine registers are write-only; verify their effect through rendering.
let mut f = Framebuffer::default();
m.memory().render_frame(&mut f).unwrap();
assert_eq!(f.pixels()[0], rgb(0x3e0));
}
#[test]
fn object_service_writes_oam_matrix_without_changing_sprite_attributes() {
let data = obj_bytes(&[Obj {
scale: [-256, 256],
angle: 0,
}]);
let (mut m, pc) = prepare(0x0f, true, [DATA, OAM + 6, 1, 8], 0x1f, &data);
m.memory_mut().write16(DISPCNT, 0x1040).unwrap();
m.memory_mut().write16(PAL, 0x7c00).unwrap();
m.memory_mut().write16(PAL + 0x202, 31).unwrap();
m.memory_mut().write16(PAL + 0x204, 0x3e0).unwrap();
for i in 0..128 {
m.memory_mut().write16(OAM + i * 8, 0x200).unwrap();
}
m.memory_mut().write16(OAM, 0x100).unwrap();
for y in 0..8 {
for x in (0..8).step_by(4) {
m.memory_mut()
.write16(
VRAM_START + 0x10000 + y * 4 + x / 2,
if x == 0 { 0x1111 } else { 0x2222 },
)
.unwrap();
}
}
complete(&mut m, pc);
assert_eq!(m.memory().read16(OAM).unwrap(), 0x100);
assert_eq!(m.memory().read16(OAM + 8).unwrap(), 0x200);
let mut f = Framebuffer::default();
m.memory().render_frame(&mut f).unwrap();
assert_eq!(f.pixels()[1], rgb(0x3e0));
assert_eq!(f.pixels()[7], rgb(31));
}
#[test]
fn output_uses_normal_halfword_and_word_video_bus_writes() {
verify_bg(
&[Bg {
texture: [0x1234, -0x1234],
screen: [0, 0],
scale: [256, 512],
angle: 0,
}],
false,
VRAM_START + 4,
);
verify_obj(
&[Obj {
scale: [256, 512],
angle: 0x1234,
}],
true,
PAL + 4,
2,
);
}
#[test]
fn dma_and_timers_progress_while_services_defer_irq_delivery() {
for service in [0x0e, 0x0f] {
let data = if service == 0x0e {
bg_bytes(&[Bg {
texture: [0, 0],
screen: [0, 0],
scale: [256, 256],
angle: 0,
}])
} else {
obj_bytes(&[Obj {
scale: [256, 256],
angle: 0,
}])
};
let (mut m, pc) = prepare(service, false, [DATA, DEST, 1, 2], 0x1f, &data);
m.step().unwrap();
m.memory_mut().write16(IE, 8).unwrap();
m.memory_mut().write16(IME, 1).unwrap();
m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap();
m.memory_mut().write32(SOURCE, 0x8765_4321).unwrap();
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
m.memory_mut().write32(DMA_BASE + 4, SOURCE + 4).unwrap();
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
let mut dma = 0;
for _ in 0..1000 {
if m.cpu().pc() == pc {
break;
}
let step = m.step().unwrap();
assert_ne!(step, StepKind::IrqEntry);
if matches!(step, StepKind::Dma { channel: 0 }) {
dma += 1;
}
}
assert_eq!(m.cpu().pc(), pc);
assert_eq!(dma, 1);
assert_eq!(m.memory().read32(SOURCE + 4).unwrap(), 0x8765_4321);
assert_eq!(m.memory().read16(IF).unwrap() & 8, 8);
assert_eq!(m.memory().read16(IME).unwrap(), 1);
assert_eq!(m.memory().read16(IRQ_FLAGS).unwrap(), 0);
assert_eq!(m.memory().read16(DEST).unwrap(), 256);
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
}
}