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

@ -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
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]
);
}
}