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:
parent
1edaa4151d
commit
f60db7e43b
5 changed files with 1020 additions and 23 deletions
81
README.md
81
README.md
|
|
@ -253,7 +253,7 @@ Verified locally on macOS 15.7.3:
|
|||
- Nix system: `aarch64-darwin`.
|
||||
- Rust host: `aarch64-apple-darwin`.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
@ -358,7 +358,7 @@ nix develop -c cargo test
|
|||
- Game Pak wait-state control (`WAITCNT`) for all three ROM windows.
|
||||
- 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 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.
|
||||
- Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`.
|
||||
- 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
|
||||
|
||||
`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.
|
||||
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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `0x11` | LZ77UnCompWram | Decompress from r0 to r1 with byte 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
**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.
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
### Timers, interrupt registers, and the machine clock
|
||||
|
|
@ -1504,6 +1560,7 @@ Important timing limits:
|
|||
| `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/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/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 |
|
||||
|
|
@ -1563,6 +1620,7 @@ Important timing limits:
|
|||
| `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/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/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 |
|
||||
|
|
@ -1636,12 +1694,12 @@ To test the core without building the window dependency:
|
|||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -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.
|
||||
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.
|
||||
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 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 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 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.
|
||||
|
|
|
|||
88
src/bios.rs
88
src/bios.rs
|
|
@ -1,10 +1,11 @@
|
|||
//! Original, optional ARM BIOS replacement. No Nintendo firmware is included.
|
||||
//! Supports interrupt waits, integer/fixed-point arithmetic, memory copy/fill, bit unpacking,
|
||||
//! LZ77/run-length/Huffman decoding, and differential filters.
|
||||
//! Supports interrupt waits, integer/fixed-point arithmetic, affine matrices,
|
||||
//! 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.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod affine;
|
||||
mod angles;
|
||||
mod arithmetic;
|
||||
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.
|
||||
/// Services support User/System callers and preserve caller status. Registers
|
||||
/// other than each arithmetic service's documented outputs are preserved.
|
||||
|
|
@ -126,6 +128,8 @@ pub fn image() -> Vec<u8> {
|
|||
(0x0a, "arctan2"),
|
||||
(0x0b, "cpu_set"),
|
||||
(0x0c, "cpu_fast_set"),
|
||||
(0x0e, "bg_affine"),
|
||||
(0x0f, "obj_affine"),
|
||||
(0x10, "bit_unpack"),
|
||||
(0x11, "lz_wram"),
|
||||
(0x12, "lz_vram"),
|
||||
|
|
@ -226,7 +230,11 @@ pub fn image() -> Vec<u8> {
|
|||
["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);
|
||||
affine::emit(&mut a);
|
||||
angles::emit(&mut a);
|
||||
bit_unpack::emit(&mut a);
|
||||
lz77::emit(&mut a);
|
||||
|
|
@ -247,7 +255,9 @@ pub fn image() -> Vec<u8> {
|
|||
a.emit(UNSUPPORTED_TRAP);
|
||||
a.label("invalid_argument");
|
||||
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) {
|
||||
|
|
@ -327,7 +337,20 @@ impl ArmImage {
|
|||
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 {
|
||||
let offset = self.labels[target] as i32 - index as i32 - 2;
|
||||
assert!((-0x80_0000..0x80_0000).contains(&offset));
|
||||
|
|
@ -336,17 +359,54 @@ impl ArmImage {
|
|||
| (u32::from(link) << 24)
|
||||
| (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();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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
186
src/bios/affine.rs
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ const DEST: u32 = 0x0201_0000;
|
|||
const CALLBACK: u32 = 0x0300_0100;
|
||||
const ROM_DATA: u32 = ROM_START + 13 * 4;
|
||||
|
||||
#[path = "bios/affine.rs"]
|
||||
mod affine;
|
||||
#[path = "bios/angles.rs"]
|
||||
mod angles;
|
||||
#[path = "bios/arithmetic.rs"]
|
||||
|
|
@ -225,7 +227,7 @@ fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() {
|
|||
|
||||
#[test]
|
||||
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 error = (0..100)
|
||||
.find_map(|_| machine.step().err())
|
||||
|
|
|
|||
684
tests/bios/affine.rs
Normal file
684
tests/bios/affine.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue