feat(bios): implement selective RegisterRamReset service
Execute SWI 0x01 through original ARM code. Clear selected work RAM, palette RAM, VRAM, and OAM; force blank; reset supported display, DMA, timer, and interrupt registers through normal bus writes. Preserve caller state and excluded BIOS work RAM apart from the common SWI frame. Reject serial/sound reset flags before display or reset writes. Document the supported subset, timer counter behavior, DMA ordering, and RAM caller hazards. Add fifteen tests covering memory flags and boundaries, I/O defaults, rendering, register preservation, hidden device latches, diagnostics, and device progress. Validated on Darwin arm64: 720 default and 709 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
2dfab137a6
commit
a8944d1d5f
5 changed files with 657 additions and 15 deletions
84
README.md
84
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 705 tests pass in debug and release builds.
|
||||
- All 720 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, SoftReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters.
|
||||
- Optional original BIOS replacement: minimal boot, SoftReset, selective RegisterRamReset, 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`.
|
||||
|
|
@ -508,6 +508,7 @@ Supported software interrupt services:
|
|||
| Number | Service | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `0x00` | SoftReset | Clear BIOS work RAM and reset CPU registers/stacks; restart in ROM or RAM without returning |
|
||||
| `0x01` | RegisterRamReset | r0 selects RAM and supported I/O resets; force blank; reject serial/sound flags |
|
||||
| `0x02` | Halt | Wait for `IE & IF`; preserve IME and caller registers |
|
||||
| `0x04` | IntrWait | `r0`: discard old selected flags when nonzero; `r1`: flags to wait for |
|
||||
| `0x05` | VBlankIntrWait | IntrWait with discard enabled and the VBlank flag selected |
|
||||
|
|
@ -535,7 +536,7 @@ Only User/System callers are supported. Calls from exception modes return a diag
|
|||
Returning services preserve CPSR and every caller register except the documented arithmetic outputs.
|
||||
SoftReset instead initializes CPU registers and status as described below.
|
||||
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 and RegisterRamReset. Division and ArcTan2 use 40 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.
|
||||
|
||||
|
|
@ -597,10 +598,64 @@ The clearing loop performs ordinary word stores; stopping execution partway leav
|
|||
Active DMA can still modify memory. Software must stop conflicting transfers before requesting a restart.
|
||||
|
||||
Lower internal RAM, external RAM, palette RAM, video RAM, and sprite attribute memory are not cleared by SoftReset.
|
||||
Use separate initialization code for those regions. `RegisterRamReset` is not implemented yet.
|
||||
Use RegisterRamReset or separate initialization code for those regions.
|
||||
Calls from exception modes retain the existing unsupported-service diagnostic.
|
||||
Exact firmware timing, bus-protection behavior, and undocumented side effects remain incomplete.
|
||||
|
||||
#### RegisterRamReset
|
||||
|
||||
`RegisterRamReset` (`SWI 0x01`) selects RAM and supported I/O resets using the low byte of r0.
|
||||
Bits 8–31 are ignored. The service preserves all caller registers and status.
|
||||
Supported flags can be combined:
|
||||
|
||||
| Bit | Action |
|
||||
| --- | --- |
|
||||
| 0 | Clear external RAM: `0x02000000..0x0203ffff` |
|
||||
| 1 | Clear lower internal RAM: `0x03000000..0x03007dff`; exclude the top 512 bytes |
|
||||
| 2 | Clear palette RAM: `0x05000000..0x050003ff` |
|
||||
| 3 | Clear all 96 KiB of video RAM: `0x06000000..0x06017fff` |
|
||||
| 4 | Clear sprite attribute memory (OAM): `0x07000000..0x070003ff` |
|
||||
| 5 | Serial reset: unsupported |
|
||||
| 6 | Sound reset: unsupported |
|
||||
| 7 | Reset supported display, DMA, timer, and interrupt registers as described below |
|
||||
|
||||
**Unsupported flags:** any request containing bit 5 or 6 reaches `bios::INVALID_ARGUMENT_TRAP`.
|
||||
Validation occurs before forced blank or RAM/I/O reset writes, but after the common SWI stack save.
|
||||
This includes the common all-devices request `r0=0xff`; it is not silently treated as a successful full reset.
|
||||
Serial and audio devices remain unimplemented. The firmware's unconditional serial-data side effect is not reproduced.
|
||||
|
||||
Every supported request first writes `DISPCNT=0x0080`, including requests with no selected flags.
|
||||
This forces a white screen and clears other display-control bits.
|
||||
RAM clears then run in bit order, using ordinary word stores.
|
||||
Zeroed OAM contains regular sprites at the origin, not disabled sprites; software must configure them before enabling objects.
|
||||
|
||||
Bit 7 runs after the selected RAM clears:
|
||||
|
||||
- Disable IME.
|
||||
- Disable all four DMA channels, then zero their source, destination, count, and control registers.
|
||||
- Stop all four timers, then zero their reload and control registers.
|
||||
- Zero display registers at `0x04000004..0x04000057`, subject to normal read-only register rules.
|
||||
- Set BG2/BG3 affine PA and PD to 256; PB, PC, and programmed origins remain zero.
|
||||
- Clear IE and WAITCNT, then acknowledge IF with a halfword write of `0xffff`.
|
||||
|
||||
Timer counter reads retain the stopped count until a later enable loads the cleared reload value.
|
||||
This is normal timer-register behavior, not a host-side replacement of device state.
|
||||
Read-only display status and VCOUNT continue to reflect the advancing display clock.
|
||||
GREENSWAP, KEYINPUT/button state, POSTFLG, and BIOS IRQ communication words are not reset.
|
||||
Keypad interrupt control is not implemented. Bit 7 covers only the listed supported registers.
|
||||
|
||||
Without bit 7, device configuration remains unchanged except for DISPCNT.
|
||||
IRQ delivery stays CPU-masked during the call. The service restores the caller's mask on return.
|
||||
Timers and DMA continue until their reset writes, if selected; this service does not stop clocks or replace frame-capture history.
|
||||
Pending IRQs remain pending without bit 7 and can be delivered after return.
|
||||
|
||||
The service uses only the common 28-byte Supervisor frame.
|
||||
Default BIOS stacks lie in the excluded top 512 bytes, but the entry save still writes its usual frame there.
|
||||
Do not put the Supervisor stack or required return code/data in selected RAM.
|
||||
The service can erase a RAM caller's instructions and still return to that now-cleared address.
|
||||
Stop conflicting DMA transfers before a clear, even when bit 7 is selected: RAM clears occur before device-register resets.
|
||||
Stopping emulation midway leaves completed stores committed. Exact firmware write ordering and cycle counts are not reproduced.
|
||||
|
||||
#### Integer arithmetic
|
||||
|
||||
Div truncates the signed quotient toward zero. The remainder has the numerator's sign, or is zero.
|
||||
|
|
@ -849,7 +904,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.
|
||||
RegisterRamReset, HardReset, and STOP remain unimplemented.
|
||||
RegisterRamReset serial/sound flags, HardReset, and STOP remain unimplemented.
|
||||
This subset is not sufficient for Pokémon Emerald compatibility.
|
||||
|
||||
### Timers, interrupt registers, and the machine clock
|
||||
|
|
@ -1600,7 +1655,7 @@ Important timing limits:
|
|||
| `src/timer_demo.rs` | Original timer-configuration program and IRQ handler |
|
||||
| `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/reset.rs` | Emitted ARM SoftReset, BIOS work RAM clearing, bank initialization, and ROM/RAM restart |
|
||||
| `src/bios/reset.rs` | Emitted ARM SoftReset and selective RegisterRamReset; CPU banks, RAM clearing, and supported device-register resets |
|
||||
| `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 |
|
||||
|
|
@ -1661,6 +1716,7 @@ Important timing limits:
|
|||
| `tests/dma.rs` | DMA widths, latches, priority, triggers, IRQs, timing, errors, and CPU integration |
|
||||
| `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/ram_reset.rs` | Selective RAM boundaries, flag combinations, I/O reset, display rendering, DMA/timer latches, and diagnostics |
|
||||
| `tests/bios/reset.rs` | All restart flags, exact RAM boundaries, CPU banks, restart execution, device continuity, and IRQ masking |
|
||||
| `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 |
|
||||
|
|
@ -1737,12 +1793,12 @@ To test the core without building the window dependency:
|
|||
direnv exec . cargo test --locked --no-default-features
|
||||
```
|
||||
|
||||
This runs 694 core and integration tests; the eleven desktop and command-line tests are excluded.
|
||||
This runs 709 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 705 tests.
|
||||
The default suite has 720 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.
|
||||
|
|
@ -1817,6 +1873,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.
|
||||
RegisterRamReset tests check full memory regions and all memory-flag combinations from ARM and Thumb.
|
||||
They verify forced blank, reserved flag bits, caller preservation, protected BIOS RAM, and serial/sound diagnostics.
|
||||
Rendering tests check write-only display state, both identity matrices, and the effect of zeroed sprite attributes.
|
||||
Device tests check DMA address/count latches, stopped timer counters, cleared reloads, and IRQ acknowledgement.
|
||||
Other checks cover partial clears, preserved input/device state, and callers whose own RAM code is erased.
|
||||
SoftReset tests check every restart flag byte from ARM and Thumb, exact RAM clearing, and preserved memory.
|
||||
They seed CPU banks and verify stack pointers, link registers, saved status, caller modes, and restart status.
|
||||
Other tests execute RAM restart code, call BIOS services again, and restart from code inside the erased region.
|
||||
|
|
@ -1882,7 +1943,8 @@ 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. Add RegisterRamReset with explicit limits for unimplemented sound/serial registers; add STOP and remaining DMA device modes.
|
||||
3. Add keypad interrupt control and remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
||||
Add STOP and remaining DMA device modes with verified timing and wake-up behavior.
|
||||
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.
|
||||
|
||||
|
|
@ -1893,7 +1955,7 @@ Hardware references used for the core:
|
|||
|
||||
- [GBATEK ARM instruction cycle times](https://problemkaputt.de/gbatek-arm-cpu-instruction-cycle-times.htm).
|
||||
- [GBATEK GBA system control and WAITCNT](https://problemkaputt.de/gbatek-gba-system-control.htm).
|
||||
- [GBATEK BIOS reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm), for SoftReset RAM bounds, CPU banks, stack pointers, and restart selection.
|
||||
- [GBATEK BIOS reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm), for SoftReset state and restart selection, plus RegisterRamReset flags, RAM bounds, and forced blank.
|
||||
- [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).
|
||||
|
|
@ -1901,7 +1963,7 @@ Hardware references used for the core:
|
|||
- [VisualBoyAdvance-M BIOS services](https://github.com/visualboyadvance-m/visualboyadvance-m/blob/master/src/core/gba/internal/gbaBios.cpp), reviewed for integer affine rounding, signed matrix intermediates, and SoftReset status handling. 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.
|
||||
- [mGBA BIOS service implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c), reviewed for RegisterRamReset register defaults, angle-polynomial coefficients and quadrant conventions, BitUnPack ordering/offsets, and Huffman tree layout and packing.
|
||||
- [HALTCNT hardware-access tests](https://github.com/mgba-emu/mgba/issues/2309), for BIOS-only writes and halfword access.
|
||||
- [GBATEK display status and IRQs](https://problemkaputt.de/gbatek-lcd-i-o-interrupts-and-status.htm).
|
||||
- [GBATEK display dimensions and timings](https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Original, optional ARM BIOS replacement. No Nintendo firmware is included.
|
||||
//! Supports SoftReset, interrupt waits, integer/fixed-point arithmetic, affine matrices,
|
||||
//! Supports SoftReset, selective RegisterRamReset, interrupt waits, 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.
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ pub const IRQ_FLAGS: u32 = 0x0300_7ff8;
|
|||
pub const IRQ_HANDLER: u32 = 0x0300_7ffc;
|
||||
/// An intentional undefined instruction used for unsupported SWIs/vectors and invalid IRQ pointers.
|
||||
pub const UNSUPPORTED_TRAP: u32 = 0xe7f0_00f0;
|
||||
/// A diagnostic trap for invalid arithmetic or asset-conversion arguments.
|
||||
/// A diagnostic trap for invalid service arguments, including unsupported reset flags.
|
||||
pub const INVALID_ARGUMENT_TRAP: u32 = 0xe7f0_00f1;
|
||||
|
||||
/// Start at reset with our optional firmware. Call Machine::step to execute boot.
|
||||
|
|
@ -51,6 +51,8 @@ pub fn boot(rom: Vec<u8>) -> Result<Machine, MemoryError> {
|
|||
/// and registers other than each arithmetic service's documented outputs.
|
||||
/// SoftReset instead clears BIOS work RAM, resets specified CPU banks, and enters
|
||||
/// ROM or RAM in ARM System mode with IRQ masked. It does not reset devices.
|
||||
/// RegisterRamReset clears selected RAM and supported I/O registers. Serial/sound
|
||||
/// reset flags reach a diagnostic before display or reset writes.
|
||||
/// IRQ callbacks must preserve r4–r11, acknowledge IF, update IRQ_FLAGS for wait
|
||||
/// services, and return with BX lr. Nested IRQs and SWIs from callbacks are unsupported.
|
||||
/// Service routines use the Supervisor stack; exact firmware stack layout and
|
||||
|
|
@ -122,6 +124,7 @@ pub fn image() -> Vec<u8> {
|
|||
a.emit(0xe20c_c0ff); // AND r12,r12,#0xff
|
||||
for (number, target) in [
|
||||
(0, "soft_reset"),
|
||||
(1, "register_ram_reset"),
|
||||
(2, "halt"),
|
||||
(4, "intr_wait"),
|
||||
(5, "vblank_wait"),
|
||||
|
|
@ -238,6 +241,8 @@ pub fn image() -> Vec<u8> {
|
|||
// pool here so reset/dispatcher loads stay within ARM's 4 KiB reach.
|
||||
a.flush_literals();
|
||||
reset::emit(&mut a);
|
||||
// Both reset routines end without fallthrough; keep their literals local.
|
||||
a.flush_literals();
|
||||
arithmetic::emit(&mut a);
|
||||
affine::emit(&mut a);
|
||||
angles::emit(&mut a);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
//! Original ARM SoftReset. This is a restart service, not a hardware reset.
|
||||
//! 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");
|
||||
|
|
@ -35,4 +40,80 @@ pub(super) fn emit(a: &mut ArmImage) {
|
|||
// 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ mod differential;
|
|||
mod huffman;
|
||||
#[path = "bios/lz77.rs"]
|
||||
mod lz77;
|
||||
#[path = "bios/ram_reset.rs"]
|
||||
mod ram_reset;
|
||||
#[path = "bios/reset.rs"]
|
||||
mod reset;
|
||||
#[path = "bios/run_length.rs"]
|
||||
|
|
@ -229,7 +231,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 [1, 3, 0x0d, 0x19, 0xff] {
|
||||
for service in [3, 0x0d, 0x19, 0xff] {
|
||||
let (mut machine, _) = call(service, false, [0, 0, 0]);
|
||||
let error = (0..100)
|
||||
.find_map(|_| machine.step().err())
|
||||
|
|
|
|||
492
tests/bios/ram_reset.rs
Normal file
492
tests/bios/ram_reset.rs
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
use super::*;
|
||||
use gba_rust::{
|
||||
bios::INVALID_ARGUMENT_TRAP,
|
||||
dma::{DmaError, DMA_BASE, DMA_STRIDE},
|
||||
input::Buttons,
|
||||
io::{
|
||||
BG0CNT, BG2PA, BG2PD, BG3PA, BG3PD, BLDALPHA, BLDCNT, DISPCNT, GREENSWAP, KEYINPUT,
|
||||
WAITCNT, WININ, WINOUT,
|
||||
},
|
||||
memory::{OAM_START, PALETTE_START},
|
||||
video::Framebuffer,
|
||||
};
|
||||
|
||||
const REGIONS: [(u32, u32, u32); 5] = [
|
||||
(1, 0x0200_0000, 0x40000),
|
||||
(2, 0x0300_0000, 0x7e00),
|
||||
(4, PALETTE_START, 0x400),
|
||||
(8, VRAM_START, 0x18000),
|
||||
(16, OAM_START, 0x400),
|
||||
];
|
||||
const MARKER: u32 = 0xa55a_c33c;
|
||||
|
||||
fn literal(code: &mut Vec<u32>, register: u32, value: u32) {
|
||||
code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]);
|
||||
}
|
||||
|
||||
fn prepare(flags: u32, thumb: bool, status: u32) -> (Machine, u32) {
|
||||
prepare_at(flags, thumb, status, None)
|
||||
}
|
||||
|
||||
fn prepare_at(flags: u32, thumb: bool, status: u32, location: Option<u32>) -> (Machine, u32) {
|
||||
let mut code = Vec::new();
|
||||
literal(&mut code, 0, flags);
|
||||
for r in 1..=12 {
|
||||
code.push(0xe3a0_0080 | r << 12 | r);
|
||||
}
|
||||
code.extend([0xe328_f20f, 0xe321_f000 | status]);
|
||||
let swi_pc = if let Some(address) = location {
|
||||
literal(&mut code, 14, address | u32::from(thumb));
|
||||
code.push(0xe12f_ff1e);
|
||||
address
|
||||
} else {
|
||||
code.extend([
|
||||
if thumb { 0xe28f_e001 } else { 0xe1a0_e00f },
|
||||
if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 },
|
||||
]);
|
||||
let address = ROM_START + code.len() as u32 * 4;
|
||||
code.extend([if thumb { 0xe7fe_df01 } else { 0xef01_0000 }, 0xeaff_fffe]);
|
||||
address
|
||||
};
|
||||
let mut m = bios::boot(words(&code)).unwrap();
|
||||
if let Some(address) = location {
|
||||
m.memory_mut()
|
||||
.write32(address, if thumb { 0xe7fe_df01 } else { 0xef01_0000 })
|
||||
.unwrap();
|
||||
m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap();
|
||||
}
|
||||
reach(&mut m, swi_pc, 200);
|
||||
(m, swi_pc + if thumb { 2 } else { 4 })
|
||||
}
|
||||
|
||||
fn complete(m: &mut Machine, pc: u32) {
|
||||
let before = m.cpu().clone();
|
||||
m.step().unwrap();
|
||||
reach(m, pc, 400_000);
|
||||
assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]);
|
||||
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
||||
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80);
|
||||
}
|
||||
|
||||
fn samples(start: u32, bytes: u32) -> [u32; 4] {
|
||||
[start, start + 4, start + bytes / 2, start + bytes - 4]
|
||||
}
|
||||
|
||||
fn seed_samples(m: &mut Machine) {
|
||||
for (_, start, bytes) in REGIONS {
|
||||
for address in samples(start, bytes) {
|
||||
m.memory_mut().write32(address, MARKER).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_samples(m: &Machine, flags: u32) {
|
||||
for (bit, start, bytes) in REGIONS {
|
||||
for address in samples(start, bytes) {
|
||||
assert_eq!(
|
||||
m.memory().read32(address).unwrap(),
|
||||
if flags & bit == 0 { MARKER } else { 0 },
|
||||
"flags={flags:#x}, address={address:#x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_memory_flag_clears_its_entire_region_and_no_other_region() {
|
||||
for thumb in [false, true] {
|
||||
for (flag, _, _) in REGIONS {
|
||||
let (mut m, pc) = prepare(flag, thumb, 0x1f);
|
||||
for (_, start, bytes) in REGIONS {
|
||||
for address in (start..start + bytes).step_by(4) {
|
||||
m.memory_mut().write32(address, MARKER ^ address).unwrap();
|
||||
}
|
||||
}
|
||||
complete(&mut m, pc);
|
||||
for (bit, start, bytes) in REGIONS {
|
||||
for address in (start..start + bytes).step_by(4) {
|
||||
assert_eq!(
|
||||
m.memory().read32(address).unwrap(),
|
||||
if bit == flag { 0 } else { MARKER ^ address },
|
||||
"flag={flag}, address={address:#x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_memory_flag_combinations_are_independent_from_arm_and_thumb() {
|
||||
for thumb in [false, true] {
|
||||
for flags in 0..32 {
|
||||
let (mut m, pc) = prepare(flags, thumb, 0x1f);
|
||||
seed_samples(&mut m);
|
||||
complete(&mut m, pc);
|
||||
check_samples(&m, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_flags_still_force_blank_and_clear_other_display_control_bits() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, pc) = prepare(0, thumb, 0x1f);
|
||||
seed_samples(&mut m);
|
||||
m.memory_mut().write16(DISPCNT, 0x1403).unwrap();
|
||||
m.memory_mut().write16(BG0CNT, 0x1234).unwrap();
|
||||
complete(&mut m, pc);
|
||||
check_samples(&m, 0);
|
||||
assert_eq!(m.memory().read16(BG0CNT).unwrap(), 0x1204);
|
||||
let mut frame = Framebuffer::default();
|
||||
m.memory().render_frame(&mut frame).unwrap();
|
||||
assert!(frame.pixels().iter().all(|p| *p == 0xffffff));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_high_bits_are_ignored_and_original_argument_is_restored() {
|
||||
for flags in [
|
||||
0x8000_0000,
|
||||
0xffff_ff00,
|
||||
0x1234_5604,
|
||||
0xffff_ff80,
|
||||
0xffff_ff9f,
|
||||
] {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, pc) = prepare(flags, thumb, 0x1f);
|
||||
seed_samples(&mut m);
|
||||
complete(&mut m, pc);
|
||||
check_samples(&m, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_ram_clear_preserves_bios_work_area_except_common_swi_frame() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, pc) = prepare(0x9f, thumb, 0x1f);
|
||||
for address in (0x0300_7e00..0x0300_8000).step_by(4) {
|
||||
m.memory_mut().write32(address, MARKER ^ address).unwrap();
|
||||
}
|
||||
complete(&mut m, pc);
|
||||
for address in (0x0300_7e00..0x0300_8000).step_by(4) {
|
||||
if (bios::SVC_STACK - 28..bios::SVC_STACK).contains(&address) {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(
|
||||
m.memory().read32(address).unwrap(),
|
||||
MARKER ^ address,
|
||||
"{address:#x}"
|
||||
);
|
||||
}
|
||||
assert_eq!(m.memory().read32(0x0300_7dfc).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_serial_sound_requests_fail_before_forced_blank_or_memory_clear() {
|
||||
for thumb in [false, true] {
|
||||
for flags in (0..256).filter(|flags| flags & 0x60 != 0) {
|
||||
let (mut m, _) = prepare(flags, thumb, 0x1f);
|
||||
seed_samples(&mut m);
|
||||
m.memory_mut().write16(DISPCNT, 0x0403).unwrap();
|
||||
for _ in 0..1000 {
|
||||
let before = m.cpu().clone();
|
||||
let cycles = m.cycles();
|
||||
let timing = m.last_timing();
|
||||
if let Err(error) = m.step() {
|
||||
assert!(matches!(
|
||||
error,
|
||||
MachineError::Cpu(CpuError::UnsupportedInstruction {
|
||||
instruction: INVALID_ARGUMENT_TRAP,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(m.cpu(), &before);
|
||||
assert_eq!(m.cycles(), cycles);
|
||||
assert_eq!(m.last_timing(), timing);
|
||||
assert_eq!(m.step(), Err(error));
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(m.step().is_err());
|
||||
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x0403);
|
||||
check_samples(&m, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() {
|
||||
for thumb in [false, true] {
|
||||
for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] {
|
||||
for flags in [0, 4, 0x80] {
|
||||
let (mut m, pc) = prepare(flags, thumb, status);
|
||||
assert!(m.cpu().registers()[1..13].iter().all(|r| *r != 0));
|
||||
complete(&mut m, pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn io_reset_clears_readable_controls_but_keeps_input_postflg_and_green_swap() {
|
||||
for flags in [0x80, 0x9f] {
|
||||
let (mut m, pc) = prepare(flags, false, 0x9f);
|
||||
m.memory_mut().set_buttons(Buttons::from_bits(0x251));
|
||||
let keys = m.memory().read16(KEYINPUT).unwrap();
|
||||
m.memory_mut().write16(GREENSWAP, 1).unwrap();
|
||||
for address in [
|
||||
BG0CNT,
|
||||
BG0CNT + 2,
|
||||
BG0CNT + 4,
|
||||
BG0CNT + 6,
|
||||
WININ,
|
||||
WINOUT,
|
||||
BLDCNT,
|
||||
BLDALPHA,
|
||||
WAITCNT,
|
||||
IE,
|
||||
] {
|
||||
m.memory_mut().write16(address, 0xffff).unwrap();
|
||||
}
|
||||
m.memory_mut().write16(DISPSTAT, 0x7b38).unwrap();
|
||||
m.memory_mut().write16(IME, 1).unwrap();
|
||||
// Overflow queues IF, independently of CPU execution.
|
||||
m.memory_mut().write32(TIMER_BASE, 0x00c0_ffff).unwrap();
|
||||
m.memory_mut().advance_cycles(1);
|
||||
assert_ne!(m.memory().read16(IF).unwrap(), 0);
|
||||
let cycles = m.cycles();
|
||||
complete(&mut m, pc);
|
||||
for address in [
|
||||
BG0CNT,
|
||||
BG0CNT + 2,
|
||||
BG0CNT + 4,
|
||||
BG0CNT + 6,
|
||||
WININ,
|
||||
WINOUT,
|
||||
BLDCNT,
|
||||
BLDALPHA,
|
||||
WAITCNT,
|
||||
IE,
|
||||
IF,
|
||||
IME,
|
||||
] {
|
||||
assert_eq!(m.memory().read16(address).unwrap(), 0, "{address:#x}");
|
||||
}
|
||||
assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0);
|
||||
assert_eq!(m.memory().read16(KEYINPUT).unwrap(), keys);
|
||||
assert_eq!(m.memory().read16(POSTFLG).unwrap(), 1);
|
||||
assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1);
|
||||
assert!(m.cycles() > cycles);
|
||||
assert!(!m.halted());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn io_reset_clears_write_only_display_state_and_restores_both_identity_matrices() {
|
||||
let (mut m, pc) = prepare(0x80, true, 0x1f);
|
||||
let mut reference = Memory::new(vec![]).unwrap();
|
||||
for (_, start, bytes) in [REGIONS[2], REGIONS[3]] {
|
||||
for offset in (0..bytes).step_by(2) {
|
||||
let value = ((offset * 37 + 1) ^ (offset >> 4)) as u16;
|
||||
m.memory_mut().write16(start + offset, value).unwrap();
|
||||
reference.write16(start + offset, value).unwrap();
|
||||
}
|
||||
}
|
||||
for address in (BG0CNT..0x0400_0058).step_by(2) {
|
||||
m.memory_mut().write16(address, 0x5353).unwrap();
|
||||
}
|
||||
complete(&mut m, pc);
|
||||
for address in [BG2PA, BG2PD, BG3PA, BG3PD] {
|
||||
reference.write16(address, 256).unwrap();
|
||||
}
|
||||
for control in [
|
||||
0x0100, 0x0200, 0x0400, 0x0800, 0x0402, 0x0802, 0x0403, 0x2403, 0x4403, 0x6403,
|
||||
] {
|
||||
for bus in [m.memory_mut(), &mut reference] {
|
||||
bus.write16(DISPCNT, control).unwrap();
|
||||
// Enable mosaic in each background without altering its offset/matrix.
|
||||
for address in (BG0CNT..=BG0CNT + 6).step_by(2) {
|
||||
bus.write16(address, 0x40).unwrap();
|
||||
}
|
||||
}
|
||||
let mut actual = Framebuffer::default();
|
||||
let mut expected = Framebuffer::default();
|
||||
m.memory().render_frame(&mut actual).unwrap();
|
||||
reference.render_frame(&mut expected).unwrap();
|
||||
assert_eq!(actual.pixels(), expected.pixels(), "DISPCNT={control:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_reset_stops_counters_clears_reload_and_does_not_replace_counter_state() {
|
||||
let (mut m, pc) = prepare(0x80, false, 0x1f);
|
||||
for timer in 0..4 {
|
||||
m.memory_mut()
|
||||
.write32(TIMER_BASE + timer * 4, 0x0080_4321)
|
||||
.unwrap();
|
||||
}
|
||||
complete(&mut m, pc);
|
||||
for timer in 0..4 {
|
||||
let address = TIMER_BASE + timer * 4;
|
||||
assert_eq!(m.memory().read16(address + 2).unwrap(), 0);
|
||||
let counter = m.memory().read16(address).unwrap();
|
||||
assert!(counter >= 0x4321);
|
||||
m.memory_mut().advance_cycles(1000);
|
||||
assert_eq!(m.memory().read16(address).unwrap(), counter);
|
||||
m.memory_mut().write16(address + 2, 0x80).unwrap();
|
||||
assert_eq!(m.memory().read16(address).unwrap(), 0); // Reload latch was cleared.
|
||||
m.memory_mut().write16(address + 2, 0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_reset_disables_channels_and_clears_write_only_address_and_count_latches() {
|
||||
for channel in 0..4 {
|
||||
let (mut m, pc) = prepare(0x80, false, 0x1f);
|
||||
let base = DMA_BASE + channel as u32 * DMA_STRIDE;
|
||||
m.memory_mut().write32(base, SOURCE).unwrap();
|
||||
m.memory_mut().write32(base + 4, DEST).unwrap();
|
||||
m.memory_mut().write32(base + 8, 0x9200_0001).unwrap(); // Repeating VBlank, count one.
|
||||
complete(&mut m, pc);
|
||||
assert_eq!(m.memory().read16(base + 10).unwrap(), 0);
|
||||
// With only control re-enabled, the source must now be zero.
|
||||
m.memory_mut().write16(base + 10, 0x8000).unwrap();
|
||||
assert_eq!(
|
||||
m.step(),
|
||||
Err(MachineError::Dma(DmaError::UnsupportedSource {
|
||||
channel,
|
||||
address: 0
|
||||
}))
|
||||
);
|
||||
m.memory_mut().write16(base + 10, 0).unwrap();
|
||||
m.memory_mut().write32(base, SOURCE).unwrap();
|
||||
m.memory_mut().write16(base + 10, 0x8000).unwrap();
|
||||
assert_eq!(
|
||||
m.step(),
|
||||
Err(MachineError::Dma(DmaError::Memory {
|
||||
channel,
|
||||
error: MemoryError::ReadOnly(0)
|
||||
}))
|
||||
);
|
||||
m.memory_mut().write16(base + 10, 0).unwrap();
|
||||
m.memory_mut().write32(base + 4, DEST).unwrap();
|
||||
m.memory_mut().write32(SOURCE, MARKER).unwrap();
|
||||
m.memory_mut().write16(base + 10, 0x8500).unwrap(); // Fixed source, words; count remains zero.
|
||||
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
|
||||
assert_eq!(m.step().unwrap(), StepKind::Dma { channel });
|
||||
assert_eq!(m.memory().read32(DEST).unwrap(), MARKER);
|
||||
assert_eq!(m.memory().read32(DEST + 4).unwrap(), MARKER);
|
||||
// Count zero expands to the channel maximum, not the stale count of one.
|
||||
assert_ne!(m.memory().read16(base + 10).unwrap() & 0x8000, 0);
|
||||
m.memory_mut().write16(base + 10, 0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_only_reset_keeps_io_and_allows_dma_timers_and_deferred_irq() {
|
||||
for thumb in [false, true] {
|
||||
let (mut m, pc) = prepare(4, thumb, 0x1f);
|
||||
let before = m.cpu().clone();
|
||||
m.step().unwrap();
|
||||
m.memory_mut().write16(BG0CNT, 0x41).unwrap();
|
||||
m.memory_mut().write16(WAITCNT, 0x4317).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, MARKER).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE, SOURCE).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 4, DEST).unwrap();
|
||||
m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap();
|
||||
let mut dma = 0;
|
||||
for _ in 0..4000 {
|
||||
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!(&m.cpu().registers()[..15], &before.registers()[..15]);
|
||||
assert_eq!(m.cpu().cpsr(), before.cpsr());
|
||||
assert_eq!(dma, 1);
|
||||
assert_eq!(m.memory().read32(DEST).unwrap(), MARKER);
|
||||
for (address, value) in [
|
||||
(BG0CNT, 0x41),
|
||||
(WAITCNT, 0x4317),
|
||||
(IE, 8),
|
||||
(IME, 1),
|
||||
(TIMER_BASE + 2, 0xc0),
|
||||
] {
|
||||
assert_eq!(m.memory().read16(address).unwrap(), value);
|
||||
}
|
||||
assert_ne!(m.memory().read16(IF).unwrap() & 8, 0);
|
||||
assert_eq!(m.step().unwrap(), StepKind::IrqEntry);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_is_visible_one_word_at_a_time_and_uses_normal_video_bus_stores() {
|
||||
for flag in [1, 2, 4, 8, 16] {
|
||||
let (mut m, pc) = prepare(flag, false, 0x1f);
|
||||
let (_, start, _) = REGIONS.into_iter().find(|r| r.0 == flag).unwrap();
|
||||
m.memory_mut().write32(start, MARKER).unwrap();
|
||||
m.memory_mut().write32(start + 4, MARKER).unwrap();
|
||||
m.step().unwrap();
|
||||
for _ in 0..1000 {
|
||||
if m.memory().read32(start).unwrap() == 0 {
|
||||
break;
|
||||
}
|
||||
m.step().unwrap();
|
||||
}
|
||||
assert_eq!(m.memory().read32(start).unwrap(), 0);
|
||||
assert_eq!(m.memory().read32(start + 4).unwrap(), MARKER);
|
||||
assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80);
|
||||
assert!(m.cpu().pc() < BIOS_SIZE as u32);
|
||||
reach(&mut m, pc, 400_000);
|
||||
assert_eq!(m.memory().read32(start + 4).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ram_callers_can_return_but_selected_ram_code_is_really_erased() {
|
||||
for thumb in [false, true] {
|
||||
for (location, clear_flag) in [(SOURCE + 0x100, 1), (0x0300_1000, 2)] {
|
||||
for flags in [4, clear_flag] {
|
||||
let (mut m, pc) = prepare_at(flags, thumb, 0x1f, Some(location));
|
||||
complete(&mut m, pc);
|
||||
assert_eq!(
|
||||
m.memory().read32(location).unwrap(),
|
||||
if flags == clear_flag {
|
||||
0
|
||||
} else if thumb {
|
||||
0xe7fe_df01
|
||||
} else {
|
||||
0xef01_0000
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleared_oam_is_zero_not_the_disabled_sprite_encoding() {
|
||||
let (mut m, pc) = prepare(16, false, 0x1f);
|
||||
m.memory_mut().write16(OAM_START, 0x200).unwrap();
|
||||
m.memory_mut().write16(PALETTE_START + 0x202, 31).unwrap();
|
||||
m.memory_mut()
|
||||
.write16(VRAM_START + 0x10000, 0x1111)
|
||||
.unwrap();
|
||||
complete(&mut m, pc);
|
||||
assert_eq!(m.memory().read16(OAM_START).unwrap(), 0);
|
||||
m.memory_mut().write16(DISPCNT, 0x1000).unwrap();
|
||||
let mut frame = Framebuffer::default();
|
||||
m.memory().render_frame(&mut frame).unwrap();
|
||||
assert_eq!(frame.pixels()[0], 0xff0000);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue