refactor: split into gameboy-rust workspace with core, demos, and desktop crates
Move the emulator into crates/gba-core, the original demo programs into crates/gba-demos, and the minifb frontend into crates/desktop. The binary is now gameboy-rust. Imports change from gba_rust to gba_core/gba_demos; no behavior changes. gba-core has no dependencies. Its integration tests use gba-demos as a development dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8944d1d5f
commit
f8a0676748
113 changed files with 304 additions and 282 deletions
119
crates/gba-core/src/bios/reset.rs
Normal file
119
crates/gba-core/src/bios/reset.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//! Original ARM SoftReset and selective RegisterRamReset services.
|
||||
//! Serial/sound reset requests are diagnostics until those devices are modeled.
|
||||
|
||||
use super::{ArmImage, IRQ_STACK, ROM_START, SVC_STACK, SYSTEM_STACK};
|
||||
use crate::{
|
||||
dma::DMA_BASE,
|
||||
io::{DISPCNT, IE, IME, TIMER_BASE},
|
||||
};
|
||||
|
||||
pub(super) fn emit(a: &mut ArmImage) {
|
||||
a.label("soft_reset");
|
||||
// Read the byte before clearing its containing RAM and the common SWI frame.
|
||||
// No stack accesses follow: this service never returns to its caller.
|
||||
a.literal(0, 0x0300_7ffa);
|
||||
a.emit(0xe5d0_2000); // LDRB r2,[r0] (zero: ROM, nonzero: EWRAM)
|
||||
a.literal(0, 0x0300_7e00);
|
||||
a.emit(0xe3a0_1080); // MOV r1,#128 (512 bytes)
|
||||
a.emit(0xe3a0_3000); // MOV r3,#0
|
||||
a.label("soft_reset_clear");
|
||||
a.emit(0xe480_3004); // STR r3,[r0],#4
|
||||
a.emit(0xe251_1001); // SUBS r1,r1,#1
|
||||
a.branch(1, "soft_reset_clear");
|
||||
|
||||
a.emit(0xe169_f003); // MSR SPSR_fc,r3 (SVC saved status = 0)
|
||||
a.emit(0xe1a0_e003); // MOV lr,r3 (SVC link = 0)
|
||||
a.literal(13, SVC_STACK);
|
||||
a.emit(0xe321_f0d2); // MSR CPSR_c,#IRQ|I|F
|
||||
a.emit(0xe169_f003); // MSR SPSR_fc,r3 (IRQ saved status = 0)
|
||||
a.emit(0xe1a0_e003); // MOV lr,r3 (IRQ link = 0)
|
||||
a.literal(13, IRQ_STACK);
|
||||
a.emit(0xe321_f0df); // MSR CPSR_c,#System|I|F
|
||||
a.literal(13, SYSTEM_STACK);
|
||||
a.emit(0xe352_0000); // CMP r2,#0
|
||||
a.literal(14, ROM_START);
|
||||
a.emit(0x13a0_e402); // MOVNE lr,#0x02000000
|
||||
for register in 0..=12 {
|
||||
a.emit(0xe3a0_0000 | register << 12); // MOV rN,#0
|
||||
}
|
||||
// Deterministic subset policy: clear NZCV and F, keep I set. Do not change
|
||||
// IME, IE, IF, devices, or unrelated CPU banks. IRQ stays masked throughout.
|
||||
a.emit(0xe329_f09f); // MSR CPSR_fc,#System|I
|
||||
a.emit(0xe12f_ff1e); // BX lr (ARM target, no return through erased SWI frame)
|
||||
|
||||
emit_ram_reset(a);
|
||||
}
|
||||
|
||||
fn emit_ram_reset(a: &mut ArmImage) {
|
||||
a.label("register_ram_reset");
|
||||
// Validate unsupported devices before forced blank or any reset writes.
|
||||
// Higher flag bits are ignored, as in the documented low-byte flag format.
|
||||
a.emit(0xe310_0060); // TST r0,#serial|sound
|
||||
a.branch(1, "invalid_argument");
|
||||
a.literal(1, DISPCNT);
|
||||
a.emit(0xe3a0_2080); // MOV r2,#forced blank (also clear other DISPCNT bits)
|
||||
a.emit(0xe1c1_20b0); // STRH r2,[r1]
|
||||
a.emit(0xe3a0_3000); // MOV r3,#0 (shared fill value)
|
||||
for (flag, start, bytes, next) in [
|
||||
(1, 0x0200_0000, 0x40000, "ram_reset_iwram"),
|
||||
(2, 0x0300_0000, 0x7e00, "ram_reset_palette"),
|
||||
(4, 0x0500_0000, 0x400, "ram_reset_vram"),
|
||||
(8, 0x0600_0000, 0x18000, "ram_reset_oam"),
|
||||
(16, 0x0700_0000, 0x400, "ram_reset_io"),
|
||||
] {
|
||||
a.emit(0xe310_0000 | flag); // TST r0,#flag
|
||||
a.branch(0, next);
|
||||
clear_words(a, start, bytes);
|
||||
a.label(next);
|
||||
}
|
||||
a.emit(0xe310_0080); // TST r0,#other registers
|
||||
a.branch(0, "return");
|
||||
|
||||
// IRQ remains CPU-masked throughout. Disable DMA/timers before clearing
|
||||
// their address/count/reload registers. These are bus writes, not host resets.
|
||||
a.literal(1, IME);
|
||||
store_halfword(a, 0); // IME = 0
|
||||
a.literal(1, DMA_BASE);
|
||||
for offset in [10, 22, 34, 46] {
|
||||
store_halfword(a, offset); // Disable each channel before clearing its registers.
|
||||
}
|
||||
clear_words(a, DMA_BASE, 48);
|
||||
a.literal(1, TIMER_BASE);
|
||||
for offset in [2, 6, 10, 14] {
|
||||
store_halfword(a, offset); // Stop each timer; the stopped counter remains latched.
|
||||
}
|
||||
clear_words(a, TIMER_BASE, 16); // Clear reload/control, not hidden counter/clock state.
|
||||
clear_words(a, DISPCNT + 4, 0x54); // Display controls, offsets, origins, windows/effects.
|
||||
a.literal(1, DISPCNT);
|
||||
a.emit(0xe3a0_3c01); // MOV r3,#256 (identity affine PA/PD for BG2 and BG3)
|
||||
for offset in [0x20, 0x26, 0x30, 0x36] {
|
||||
store_halfword(a, offset);
|
||||
}
|
||||
a.emit(0xe3a0_3000); // MOV r3,#0
|
||||
a.literal(1, IE);
|
||||
store_halfword(a, 0); // IE = 0
|
||||
store_halfword(a, 4); // WAITCNT = 0
|
||||
a.literal(3, 0xffff);
|
||||
store_halfword(a, 2); // Acknowledge IF after stopping timer/display IRQ sources.
|
||||
a.branch(14, "return");
|
||||
|
||||
// Leaf routine; only r1/r2 change. The common 28-byte SWI frame saves lr.
|
||||
a.label("ram_reset_words");
|
||||
a.emit(0xe481_3004); // STR r3,[r1],#4
|
||||
a.emit(0xe252_2004); // SUBS r2,r2,#4
|
||||
a.branch(1, "ram_reset_words");
|
||||
a.emit(0xe12f_ff1e); // BX lr
|
||||
}
|
||||
|
||||
fn clear_words(a: &mut ArmImage, address: u32, bytes: u32) {
|
||||
debug_assert!(bytes != 0 && bytes % 4 == 0);
|
||||
a.literal(1, address);
|
||||
a.literal(2, bytes);
|
||||
a.call("ram_reset_words");
|
||||
}
|
||||
|
||||
// Store r3 through r1 with an ARM immediate halfword offset.
|
||||
fn store_halfword(a: &mut ArmImage, offset: u32) {
|
||||
debug_assert!(offset <= 255 && offset % 2 == 0);
|
||||
a.emit(0xe1c1_30b0 | (offset & 0xf0) << 4 | (offset & 15));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue