docs: split README into docs/ and add AGENTS.md

Move the README's hardware notes, demo controls, environment setup, test
notes, file map, and references into docs/ and docs/hardware/. The README
is now an overview with a docs index. AGENTS.md gives coding agents a short
map of crates, commands, and where to read and write documentation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
0xrsydn 2026-10-05 16:18:37 +07:00
commit 7d396fea35
15 changed files with 2139 additions and 2009 deletions

433
docs/hardware/bios.md Normal file
View file

@ -0,0 +1,433 @@
# BIOS replacement
The original, Nintendo-free BIOS image built by `crates/gba-core/src/bios/` and the services it implements.
`crates/gba-core/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.
Use `bios::boot(rom)` to create a machine at the reset vector with this image mapped.
Then call `Machine::step()` to execute its minimal boot sequence.
`bios::image()` also exposes the bytes for `Memory::with_bios`; start with `Cpu::at_reset()` to initialize its stacks.
Starting directly at a cartridge with uninitialized banked stacks does not prepare these services.
Existing caller-supplied BIOS images and `Memory::new` behavior remain unchanged.
Minimal boot initializes:
- System stack: `0x03007f00`.
- IRQ stack: `0x03007fa0`.
- Supervisor stack: `0x03007fe0`.
- BIOS IRQ flags at `0x03007ff8` and the callback pointer at `0x03007ffc`: zero.
- IME: zero; POSTFLG: one.
It then enters `0x08000000` in ARM System mode with IRQ/FIQ masks clear.
It does not reproduce Nintendo's logo, cartridge-header checks, RAM clearing, boot delays, or full hardware initialization.
The executable still does not load game files.
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 |
| `0x06` | Div | Signed `r0 / r1`; return quotient in r0, remainder in r1, and quotient magnitude in r3 |
| `0x07` | DivArm | Div with the incoming numerator and denominator exchanged |
| `0x08` | Sqrt | Unsigned integer square root of r0; return the rounded-down result in r0 |
| `0x09` | ArcTan | Signed fixed-point tangent in r0; return a signed angle in r0 |
| `0x0a` | ArcTan2 | Signed fixed-point X/Y in r0/r1; return the unsigned direction angle in r0 |
| `0x0b` | CpuSet | `r0`: source; `r1`: destination; `r2`: count, fill, and width control |
| `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 |
| `0x13` | HuffUnComp | Decode 4-bit or 8-bit symbols from r0 to r1 with word writes |
| `0x14` | RLUnCompWram | Expand run-length data from r0 to r1 with byte writes |
| `0x15` | RLUnCompVram | Expand run-length data from r0 to r1 with buffered halfword writes |
| `0x16` | Diff8bitUnFilterWrite8bit | Reconstruct 8-bit samples from r0 to r1 with byte writes |
| `0x17` | Diff8bitUnFilterWrite16bit | Reconstruct 8-bit samples from r0 to r1 with buffered halfword writes |
| `0x18` | Diff16bitUnFilter | Reconstruct 16-bit samples from r0 to r1 with halfword writes |
ARM code uses `SWI service_number << 16`; Thumb code uses the service number directly.
Only User/System callers are supported. Calls from exception modes return a diagnostic rather than risking nested stack corruption.
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 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.
**IRQ callback contract:** install a word-aligned ARM callback address at `0x03007ffc` after boot.
The dispatcher saves r0–r3, r12, and lr on the IRQ stack, then calls it with r0 equal to `0x04000000`.
The callback must preserve r4–r11, acknowledge handled IF bits, and return with `BX lr`.
For interrupt waits, it must also OR the handled bits into the halfword at `0x03007ff8`.
Acknowledging IF alone does not complete IntrWait. Nested IRQs and SWIs from callbacks are not supported.
The dispatcher restores registers and returns with `SUBS pc, lr, #4`.
IntrWait consumes only the selected BIOS RAM flags; other flags remain set.
With `r0=0`, a previously recorded selected flag permits immediate return.
Otherwise the service enters HALT and permits IRQ callbacks to record new flags.
It masks IME around the flag check and HALT write to avoid losing a wake-up between them.
IME returns enabled, while the caller's CPSR and instruction state are restored.
These routines follow the documented wait contract, not every known firmware implementation quirk.
CpuSet uses the low 21 bits of r2 as its halfword/word count, bit 24 for fill, and bit 26 for word width.
CpuFastSet uses the same count/fill fields but always transfers words using eight-register block loads and stores.
Fill reads its source value once. Zero count performs no source or destination accesses.
Addresses align down to the transfer width; callers should still supply aligned addresses.
Sources below `0x02000000`, or ranges with a wrapping end address, return without copying.
Other invalid accesses use normal memory diagnostics. Earlier successful writes remain committed if a later instruction fails.
These copy services keep CPU IRQ delivery masked until return; device clocks and DMA continue throughout.
BIOS bus protection, exact firmware timing, undocumented side effects, and nested service calls remain incomplete.
## SoftReset
`SoftReset` (`SWI 0x00`) does not return to its caller.
It reads the byte at `0x03007ffa` before clearing the containing RAM:
zero selects cartridge ROM at `0x08000000`; any nonzero value selects work RAM at `0x02000000`.
Surrounding bytes do not affect this choice. Both destinations start in ARM state, even for Thumb callers.
The service clears exactly `0x03007e00..0x03007fff`, including BIOS IRQ flags, the callback pointer, and the restart flag.
It resets r0–r12 to zero and initializes these CPU banks:
| Mode | Stack pointer | Link register | Saved status |
| --- | --- | --- | --- |
| System/User | `0x03007f00` | Selected restart address | Not present |
| Supervisor | `0x03007fe0` | Zero | Zero |
| IRQ | `0x03007fa0` | Zero | Zero |
Other banked registers and saved status registers remain unchanged.
The service enters System mode with CPSR `0x9f`: IRQ masked, FIQ unmasked, condition flags clear.
This is the subset's deterministic status policy, not independently verified hardware behavior.
It jumps through the System link register and does not execute the cold-boot sequence.
SoftReset does not reset I/O registers or devices.
IME, IE, pending IF bits, POSTFLG, display configuration, and timer configuration remain in place.
Device clocks and DMA continue, so counters and pending requests can change during the service.
IRQ delivery stays masked throughout the service and after restart.
Restart code must install an IRQ callback and configure interrupts before unmasking IRQ.
The common SWI entry still requires an initialized, writable Supervisor stack and saves a 28-byte frame.
SoftReset abandons that frame. It uses no further stack storage and never restores the old caller state.
With default stacks, the frame lies inside the cleared RAM.
If the Supervisor stack is outside this region, its entry writes remain there after reset.
The clearing loop performs ordinary word stores; stopping execution partway leaves a partially cleared region.
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 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.
DivArm takes the denominator in r0 and numerator in r1, then returns the same outputs as Div.
Both return the unsigned absolute quotient in r3 and preserve r2 and r4–r14.
`INT_MIN / -1` returns `0x80000000` in r0 and r3, with a zero remainder.
Division uses a fixed 32-iteration integer algorithm. It does not use host arithmetic to calculate results.
Division by zero reaches `bios::INVALID_ARGUMENT_TRAP` instead of reproducing the firmware's possible endless loop.
Sqrt treats r0 as an unsigned 32-bit number and returns an unsigned result in `0..65535`.
The result satisfies `root² <= input < (root + 1)²`. Other registers are preserved.
Its integer algorithm uses 16 iterations without floating-point calculations.
Arithmetic services keep IRQ delivery masked until return, without changing IME.
Device clocks and DMA continue. Instruction costs do not reproduce the original BIOS algorithms' timing.
## Fixed-point angles
`ArcTan` (`SWI 0x09`) and `ArcTan2` (`SWI 0x0a`) accept signed 16-bit fixed-point inputs with 14 fractional bits.
An input value of `0x4000` represents 1.0. This format is also called signed Q2.14.
Inputs must be sign-extended into their 32-bit registers. For example, -1 uses `0xffffffff`, not `0x0000ffff`.
This subset rejects values outside `-32768..32767` through `bios::INVALID_ARGUMENT_TRAP`, rather than interpreting undocumented wider inputs.
Angles use 65,536 units per full turn. A quarter turn is `0x4000`; a half turn is `0x8000`.
ArcTan reads the tangent from r0 and returns a sign-extended signed angle in r0.
For example, tangents +1.0 and -1.0 return +`0x2000` and -`0x2000`, respectively.
The routine evaluates a fixed-point polynomial using low-32-bit products and arithmetic shifts.
Negative intermediate results round down. The implementation does not substitute a host floating-point calculation.
The polynomial has poor accuracy for tangent magnitudes above 1.0; this known limitation is retained.
ArcTan2 reads X from r0 and Y from r1. It returns an unsigned angle in `0..65535` in r0.
It divides the smaller coordinate magnitude by the larger, then applies the shared polynomial and quadrant correction.
The signed ratio truncates toward zero and stays within `[-1.0, 1.0]` before polynomial evaluation.
Axes return exact quarter-turn angles. The zero vector returns zero without division.
Use ArcTan2 for direction calculations that require all four quadrants.
Both services preserve r1–r14 and the caller's status. Undocumented BIOS scratch-register outputs are not reproduced.
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.
Each flag byte describes eight tokens, most significant bit first.
A literal token produces one byte. A reference token copies 3–18 bytes from 1–4,096 bytes behind the output position.
References can overlap previously produced output. Reads observe each preceding write through the normal emulated bus.
Use separate compressed-source and output buffers; overlapping those buffers is not supported.
The WRAM variant writes bytes and accepts byte-aligned destinations.
Use it only with memory that supports byte writes, such as work RAM. Video/palette RAM byte-write rules still apply.
The VRAM variant also works in work RAM, but always combines two bytes into a halfword store.
This subset requires an even destination address and an even output length for that variant.
It rejects distance-one references, which depend on a previous byte that may still be buffered.
A lone buffered byte is not committed if the next source read fails.
The emulator does not model firmware quirks for odd output sizes or invalid references.
The decoder validates the type byte, source alignment, source protection, output address wrap, and reference bounds.
Sources below `0x02000000`, references before produced output, and runs beyond the declared size reach `bios::INVALID_ARGUMENT_TRAP`.
Invalid VRAM alignment, odd output size, and distance-one references reach the same diagnostic trap.
Zero output length reads no tokens and writes no output after header/argument validation.
Truncated cartridge input and invalid destination accesses return normal memory diagnostics.
Earlier completed writes remain committed on failure; neither the whole service nor the whole output buffer is rolled back.
Each call runs as ordinary ARM instructions, so callers can enforce machine-step limits on large streams.
LZ77 preserves caller registers/status and keeps CPU IRQ delivery masked until return; device clocks and DMA continue.
## Run-length decompression
Both variants read a word-aligned header at r0. Its low byte must be `0x30`; the upper 24 bits specify output length.
Each block starts with a control byte:
- Bit 7 clear: copy the following `(control & 127) + 1` literal bytes, from 1 to 128 bytes.
- Bit 7 set: read one value and repeat it `(control & 127) + 3` times, from 3 to 130 bytes.
Blocks can alternate freely. Repeated values are read once, rather than read back from the destination.
Decoding stops at the declared output length without reading padding or another block.
Use separate source and destination buffers; overlapping those buffers is not supported.
`RLUnCompWram` accepts byte-aligned destinations and uses normal byte stores. Use memory that supports byte writes.
`RLUnCompVram` buffers bytes across block boundaries and commits complete halfwords, including when the destination is work RAM.
This subset requires an even destination and an even output length for the halfword variant.
A lone buffered byte is not written after a source failure. Odd-size firmware behavior remains unmodeled.
Invalid type bytes, source alignment/protection, output address wrap, and halfword alignment/size reach `bios::INVALID_ARGUMENT_TRAP`.
Sources below `0x02000000` are rejected. Each block must fit the remaining declared output before its payload is read.
An oversized block returns a diagnostic, rather than reproducing firmware behavior on malformed data.
Zero length reads no blocks and writes no output after header/argument validation.
Truncated source data and invalid destination accesses use normal memory diagnostics. Earlier completed writes remain committed.
The CPU executes these routines as original ARM instructions, sharing the byte/halfword output routine with LZ77.
They preserve caller registers/status and keep CPU IRQ delivery masked until return, without changing IME.
Timers, display clocks, and DMA continue during execution. Callers can enforce machine-step limits for large streams.
Exact Nintendo BIOS timing and undocumented side effects are not reproduced.
## Huffman decompression
`HuffUnComp` (`SWI 0x13`) decodes binary-tree paths into 4-bit or 8-bit symbols.
The source at r0 and the destination at r1 must be word-aligned.
The header's low byte must be `0x24` or `0x28`. Its upper 24 bits specify output length in bytes.
This subset requires a multiple of four output bytes and uses ordinary word stores.
The byte at source offset 4 holds the tree size. The root node follows at offset 5.
The size byte and tree table occupy `2 × (size + 1)` bytes together, including any tree padding.
The compressed bitstream starts immediately after this section. This subset requires that address to be word-aligned; it does not round it.
The table can contain up to 511 bytes after its size byte.
Each internal node uses these fields:
- Bits 0–5 hold a forward child offset.
- The left child address is `(node address & ~1) + 2 × (offset + 1)`.
- The right child follows one byte later.
- Bit 7 marks the left child as a leaf. Bit 6 marks the right child as a leaf.
Input words use little-endian byte order, but branch bits are consumed from bit 31 down to bit 0.
Zero selects the left child; one selects the right child. A leaf contains the decoded symbol.
The decoder returns to the root after each symbol. Internal-node state survives input-word refills.
Output symbols fill each word from its lowest bits upward. For 4-bit output, the first symbol occupies the low nibble.
The decoder validates only visited edges and leaves. It does not scan unused branches or padding.
Traversed children must stay inside the declared table. A visited 4-bit leaf must not contain upper bits.
Invalid headers, widths, alignment, output size, protected sources, wrapping addresses, and invalid tree references reach `bios::INVALID_ARGUMENT_TRAP`.
Sources below `0x02000000` are rejected. These checks are development safeguards, not exact firmware behavior on malformed streams.
Zero output length reads no tree or bitstream after header/argument validation.
Decoding stops at the declared output length without consuming remaining branch bits or trailing words.
Truncated input and invalid stores use normal memory diagnostics. Earlier complete output words remain committed; pending output is discarded on failure.
Use separate source and output buffers. Overlapping buffers and partial final output words are not supported.
The CPU executes original ARM instructions for all traversal and packing. No host-side service hook performs decoding.
The routine preserves caller registers/status and keeps CPU IRQ delivery masked until return. IME is unchanged; device clocks and DMA continue.
Exact Nintendo BIOS timing and undocumented side effects remain unmodeled.
## Bit unpacking
`BitUnPack` (`SWI 0x10`) expands packed values, such as monochrome font pixels, into wider destination units.
`r0` points to source bytes. `r1` points to word-aligned output. `r2` points to this eight-byte descriptor:
| Offset | Field |
| --- | --- |
| `+0` | 16-bit source length in bytes, from 0 to 65,535 |
| `+2` | 8-bit source unit width: 1, 2, 4, or 8 bits |
| `+3` | 8-bit destination unit width: 1, 2, 4, 8, 16, or 32 bits |
| `+4` | 32-bit offset: bits 0–30 hold the added value; bit 31 enables offsets for zero units |
Source bytes require no alignment. This subset requires a word-aligned descriptor and a destination width at least as large as the source width.
Each source byte supplies its lowest unit first. Output units also fill each word from its lowest bits upward.
The routine adds the offset to nonzero source units. It adds the offset to zero units only when bit 31 is set.
The zero-data flag is not part of the numeric offset.
Output length is `source length × destination width / source width` bytes and must be a multiple of four.
The routine uses ordinary word stores, including for work RAM, video RAM, palette RAM, and OAM.
Use separate source, descriptor, and output buffers. Overlapping these buffers is not supported.
Invalid widths, narrowing, misalignment, incomplete output words, protected sources, and wrapping address ranges reach `bios::INVALID_ARGUMENT_TRAP`.
Sources below `0x02000000` are rejected. Each converted value must fit its destination width after adding the offset.
Overflow produces a diagnostic instead of masking the value or allowing bits to affect neighboring output units.
A failure discards the pending incomplete word; earlier completed stores remain committed.
These checks are development safeguards, not a model of Nintendo BIOS behavior for invalid parameters.
Zero length accesses neither source data nor output after descriptor and argument validation.
Unmapped descriptor/source reads and invalid stores use normal memory diagnostics.
The service preserves caller registers/status and uses original ARM instructions, without host-side SWI interception.
CPU IRQ delivery stays masked until return. IME is unchanged; timers, display clocks, and DMA continue.
Exact firmware timing and undocumented register outputs remain unmodeled.
## Differential filters
These services reconstruct samples from stored differences. They do not change the payload size.
The source at r0 starts with a word-aligned four-byte header:
- Low byte `0x81` selects 8-bit samples; `0x82` selects 16-bit samples.
- The upper 24 bits specify output length in bytes, including for the 16-bit service.
- The first payload unit is the first original sample. Each following unit is the difference from the previous sample.
The accumulator starts at zero. Each input unit adds to the accumulator, producing the next output sample.
Eight-bit samples wrap modulo 256; sixteen-bit samples wrap modulo 65,536. This handles positive and negative differences.
Sixteen-bit input and output units use little-endian byte order.
`Diff8bitUnFilterWrite8bit` accepts byte-aligned destinations and odd byte counts. It uses ordinary byte stores.
`Diff8bitUnFilterWrite16bit` buffers pairs of reconstructed bytes, then writes halfwords.
`Diff16bitUnFilter` reads and writes halfwords directly. Both halfword-output variants require even destination addresses and byte counts.
Use separate source and output buffers. Overlapping buffers and firmware quirks for odd halfword-output sizes are not supported.
Invalid header types/unit sizes, source alignment, protected sources, output alignment/size, and wrapping address ranges reach `bios::INVALID_ARGUMENT_TRAP`.
Sources below `0x02000000` are rejected. These strict checks are development diagnostics, not exact firmware behavior on invalid data.
After validation, zero output length reads no payload and writes no output. Decoding stops at the declared length without consuming trailing data.
Unmapped source reads and invalid output writes return normal memory diagnostics.
Earlier completed writes remain committed. The buffered byte variant does not write a lone pending byte after a source failure.
All output uses normal bus rules: byte writes to video memory can duplicate bytes, and byte writes to OAM are ignored.
These original ARM routines execute through the CPU, not a host-side service hook.
They preserve caller registers/status and mask CPU IRQ delivery until return, without changing IME.
Timers, display clocks, and DMA continue. Exact Nintendo BIOS timing and undocumented side effects remain unmodeled.
## Firmware diagnostics
Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbacks reach an intentional undefined-instruction trap.
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 serial/sound flags, HardReset, and STOP remain unimplemented.
This subset is not sufficient for Pokémon Emerald compatibility.

133
docs/hardware/cpu.md Normal file
View file

@ -0,0 +1,133 @@
# CPU: ARM7TDMI interpreter
Instruction semantics, processor modes, exceptions, and nominal timing as implemented in `crates/gba-core/src/cpu/`.
## Instruction demo and execution rules
The `--cpu-demo` command first runs the original instruction demo for 40 steps, starting at `0x08000000` in ARM state.
It counts r0 down from three while incrementing r1.
`SUBS` updates the flags. `BNE` repeats the loop until the zero flag is set.
`CMP` checks the result, and `MOVEQ` sets r2 to 42.
The program then stores 42 in work RAM at `0x02000000` and loads it into r4.
The program sets the stack pointer to `0x03000100` in internal work RAM.
A subroutine saves r4 and the link register using `STMDB sp!`.
It shifts r4 left into r5 and multiplies r4 by r5 into r6.
`SWP` writes 3528 to work RAM and returns the old value, 42, in r7.
The subroutine clears r4, then uses `LDMIA sp!` to restore r4 and return through the saved PC.
The program then uses `BX` to enter Thumb code at `0x08000064`.
A Thumb subroutine saves its return address, multiplies six by seven, and returns using `POP {pc}`.
The Thumb code stores 42 at `0x02000004`, checks it, and uses `BX` to return to ARM.
It finishes with `r0=42`, `r1=7`, `r2=42`, `r4=42`, `r5=84`, `r6=3528`, and `r7=42`.
The stack pointer returns to `0x03000100`.
The trace labels each step as `Arm` or `Thumb` and shows flags in `NZCV` order.
The final flags are `0110`.
Its last instruction branches to itself. The host stops after the fixed step count.
The command then runs an 11-step exception demo with a separate CPU and memory.
ARM and Thumb code each execute `SWI` and enter the ARM Supervisor handler through vector `0x08`.
The original test handler increments r10 and uses `MOVS pc, lr` to restore the caller's status.
The trace shows processor modes, instruction states, CPSR values, and two completed handler calls.
This test image is not a Nintendo BIOS and does not implement BIOS services.
A failed condition skips the instruction without changing registers or flags, except for advancing r15.
For subtraction, carry means no unsigned borrow occurred.
Logical flag-setting operations preserve overflow and take carry from the shifter.
Unshifted operands preserve the previous carry flag.
Arithmetic operations compute carry and overflow from the arithmetic result, not the shifter.
The CPU stores the next instruction address in r15.
In ARM state, ordinary operand reads add eight bytes. Register-specified shifts use a twelve-byte offset.
Storing r15 in ARM state also uses a twelve-byte offset.
Thumb operand reads use PC plus four. Literal loads and PC-relative addresses additionally align down to a word.
`BX` selects the instruction set; ordinary PC writes keep the current instruction set.
On ARM7, Thumb `POP {pc}` remains in Thumb even if the loaded address has bit zero clear.
`Cpu::new` starts in ARM System mode with interrupts enabled. It bypasses hardware boot.
`Cpu::at_reset()` starts at vector zero in ARM Supervisor mode, with IRQ and FIQ disabled.
Both constructors zero registers deterministically; real reset register values are not all defined.
`Cpu::instruction_set()`, `mode()`, `cpsr()`, and `spsr()` report the current processor state.
Each call to `Cpu::step(&mut memory)` executes one ARM word or one Thumb halfword, not one hardware cycle.
The two halves of Thumb `BL` execute separately. A standalone suffix uses the existing link register.
Block transfers assign the lowest-numbered register to the lowest memory address.
They align addresses down without rotating loaded words, and preserve low base bits during writeback.
On ARM7, an empty register list transfers PC but uses a 64-byte span for addressing and writeback.
`LDM` suppresses writeback when the base register is in the list.
With writeback enabled, `STM` stores the old base only when that register is first in the list.
Otherwise, `STM` stores the updated base.
## Processor status and exceptions
User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR.
Fast interrupt mode (`FIQ`) also banks r8–r12. Other modes share those registers.
`MRS` reads status. `MSR` writes selected fields; User mode can only change CPSR arithmetic flags.
Reserved status bits read as zero. Unsupported status fields have no effect.
Invalid CPSR modes and attempts to change its Thumb bit through `MSR` return diagnostics.
Use `BX` or an exception return to change instruction state.
An exception saves CPSR in the destination mode's SPSR and writes that mode's link register.
Entry selects ARM state, masks interrupt requests (`IRQ`), and branches to the exception vector.
FIQ entry also masks FIQ. Ordinary IRQ entry preserves the FIQ mask.
`MOVS pc, lr`, `SUBS pc, lr, #offset`, and `LDM` with S and PC restore CPSR from SPSR.
Return alignment follows the saved instruction state, not target bit zero.
User/System status returns fail because those modes have no SPSR.
An SPSR can contain invalid mode bits, but a return using those bits fails without changing CPU state.
Other S-bit block transfers access User registers while using the current mode's base register.
User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics.
These restrictions avoid unpredictable or unverified forms.
`Cpu::enter_exception` explicitly supports software interrupt, undefined instruction, prefetch abort, data abort, IRQ, and FIQ entry.
For synchronous exceptions, call it with PC at the faulting instruction.
For IRQ/FIQ, call it between instructions, with PC at the next instruction.
`Cpu::take_interrupt(irq, fiq)` samples supplied interrupt lines between steps and respects masks and FIQ priority.
`Machine` connects timer, display, and DMA requests to the GBA IRQ line. GBA devices do not generate FIQ.
Memory and unsupported-instruction errors do not automatically enter exceptions.
`Memory::with_bios(rom, bios)` accepts exactly 16 KiB of caller-supplied vector code.
`Memory::new(rom)` leaves the BIOS area unmapped.
Without vector code, `SWI` enters Supervisor mode, then the next fetch reports an unmapped-memory error.
Neither constructor provides Nintendo BIOS services or initializes BIOS-managed RAM.
`bios::boot(rom)` separately opts into the original firmware subset described in [bios.md](bios.md).
## Instruction and bus timing
The timing model uses ARM7 cycle summaries:
- **S:** sequential memory access.
- **N:** non-sequential memory access.
- **I:** internal CPU cycle, always one clock cycle.
An ordinary arithmetic instruction costs 1S. A register-specified shift adds 1I, even for a zero shift amount.
Loads cost a code S access, a data N access, and 1I. Stores use N for code and data.
Block transfers charge N for the first data word and S for each following word.
Loads add 1I. Loading PC also adds the branch refill cost.
Branches, PC writes, software interrupts, and exception entry use a nominal 1N+2S code refill.
A skipped conditional instruction only pays its code S access.
Thumb `BL` charges 1S for its prefix and 1N+2S for its suffix.
Multiply costs vary with the incoming multiplier's upper bytes; accumulate and long forms add internal cycles.
ARM uses Rs for this calculation. Thumb multiply uses the incoming destination register.
Data costs use the addresses and widths of actual CPU bus calls, after alignment handling.
BIOS, internal work RAM, OAM, and supported I/O accesses cost one cycle.
External work RAM costs 3 cycles for byte/halfword accesses and 6 for words.
Palette and video RAM cost 1 cycle for byte/halfword accesses and 2 for words, without display contention.
ROM costs include one cycle plus the configured wait states for each 16-bit transfer.
A ROM word uses two halfword accesses; its second halfword always uses sequential timing.
Accesses at 128 KiB ROM boundaries force non-sequential timing for the first halfword.
`WAITCNT` resets to zero and supports byte, halfword, and word access.
The writable mask is `0x5fff`; the Game Pak type flag reads as GBA, and the upper halfword reads as zero.
A CPU write to WAITCNT does not retroactively change that instruction's code-access cost.
The new settings apply to subsequent code accesses.
Important timing limits:
- Code S/N counts follow instruction summaries, not a simulated fetch pipeline.
- Ordinary code costs use the current instruction address. No speculative PC+8 fetch or startup pipeline fill is performed.
- PC writes use destination-region costs and the restored instruction width for nominal refill accesses.
- Refill cost calculation does not read target bytes. An invalid branch target fails on the following instruction fetch.
- Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution.
- PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.
- External work RAM timing is fixed. The undocumented memory-control register is not implemented.
- Exact DMA startup/resumption delays, display-bus contention, shared timer prescaler phase, and timer startup delays remain unmodeled.

View file

@ -0,0 +1,95 @@
# Display timing and scanline capture
## Display timing, status, and interrupts
The display clock runs from the same supplied cycles as the timers:
- 1,232 cycles per scanline, with 228 lines numbered 0–227.
- 160 visible lines and 280,896 cycles per complete frame.
- HBlank status starts at cycle 1,006 of every line, not at the last visible pixel's cycle 960.
- VBlank starts at line 160. Its status flag remains set through line 226 and clears at line 227.
- HBlank status and IRQ events occur on all lines, including hidden lines.
- Forced blank changes pixel output but does not stop or reset the display clock.
`DISPSTAT` at `0x04000004` contains read-only VBlank, HBlank, and VCount-match flags in bits 0–2.
Bits 3–5 enable the corresponding interrupt sources. Bits 8–15 select the comparison scanline.
Bits 6–7 are unused on GBA. The writable mask is `0xff38`.
`VCOUNT` at `0x04000006` reports the current line, 0–227. Writes are ignored.
Byte, halfword, and combined word accesses are supported. I/O registers are not mirrored.
VBlank, HBlank, and VCount-match events latch IF bits 0, 1, and 2 when their local enables are set.
IE, IME, and CPSR.I gate CPU delivery, not those pending flags.
Acknowledging IF during an active blank period does not replay the old event.
Changing the comparison value recomputes the match flag immediately.
A false-to-true match can latch its enabled IRQ; enabling an IRQ with its flag already set does not create an edge.
Comparison values 228–255 never match.
`Memory::display_position()` reports the scanline, cycle within the line, completed frames, and VBlank-entry count.
The clock starts at line zero, cycle zero, with comparison zero matched and no pending IRQ.
Both event counters wrap as 64-bit values. Bulk advances account for every crossed event, including multiple frames.
Display status is calculated at the resulting position. CPU IRQ delivery occurs between instructions, after ready DMA work.
Hardware IRQ synchronization delays and display bus contention are not modeled.
`Machine::run_until_vblank(max_steps)` executes through the next line-160 entry, even if already in VBlank.
It returns at the first instruction, DMA-unit, or HALT-idle boundary after that event and reports the number of machine steps.
DMA requested by that VBlank event can still be pending; this method does not wait for a redraw or DMA completion.
A zero or exhausted step limit returns `FrameRunError::StepLimit`.
CPU and DMA diagnostics return `FrameRunError::Cpu` and `FrameRunError::Dma`, respectively.
Completed steps remain committed on error. The method does not inspect demo RAM.
When row capture is enabled, the clock advances within those steps also render visible rows.
## Scanline frame capture
`Memory::set_scanline_rendering(true)` enables row capture without changing clocks or hardware registers.
Capture is disabled by default for CPU-only use. All CPU-driven demo frame runners enable it automatically.
Repeatedly enabling capture preserves progress. Disabling it drops captured buffers and availability state.
For each visible line, the renderer composes the row at HBlank entry, cycle 1,006.
It uses current scrolling, background VRAM, palette, and effect registers, together with previously prepared sprite samples.
Affine backgrounds use internal scanline origins. Rectangular windows combine vertical flags with retained horizontal comparator history.
Vertical mosaic sampling uses separate background and sprite counters.
The completed row is retained. Later writes cannot recolor or move it.
At entry to line 160, the completed image becomes available for presentation.
This occurs before the machine services DMA requested by that VBlank edge.
HBlank DMA similarly runs after row capture. Palette and background writes can affect the following row.
OAM and sprite-tile writes wait for a later preparation event; the following row is already prepared.
`Memory::present_frame(&mut frame)` copies the latest completed captured image without advancing time or consuming the image.
It returns `Ok(false)` when capture is disabled or no complete frame is available.
`Memory::captured_vblank()` identifies the latest fully captured frame by its VBlank-entry counter, including failed frames.
Before publication, presentation keeps returning the previous complete frame.
Capture started after row zero's HBlank discards that partial frame and waits for the next full visible frame.
Starting before row zero's HBlank can capture the current frame.
Rendering diagnostics do not turn successful CPU instructions or DMA units into failures.
The capture records the first rendering error in the frame and reports it from `present_frame` after VBlank publication.
Errors and unavailable frames leave the caller's output unchanged. A later valid frame restores normal presentation.
`Memory::render_frame` remains an independent debug snapshot API. It ignores row history and uses current state for the entire screen.
With capture enabled, clock advancement splits at HBlank entries, scanline starts, and sprite preparation events.
Large clock batches still account for each crossed row, but do not execute CPU code or service pending DMA themselves.
Unserviced DMA requests still coalesce. Rendering costs host time, not additional emulated cycles.
Capture uses two 240×160 pixel buffers. Sprite preparation separately retains two rows of indices, metadata, and coverage.
With capture disabled, a clock batch prepares at most the latest two eligible sprite rows.
The bulk clock path remains constant-time relative to the supplied cycle count.
**Timing limits:** graphics data is sampled once per row, not fetched pixel by pixel.
Horizontal window comparisons are the exception: their history records four-cycle column events.
CPU instructions and DMA writes commit before their nominal cycle batch advances.
A palette or background-data write in an instruction that crosses HBlank therefore affects the entire captured row.
Sprite preparation similarly uses writes committed before its cycle40 boundary.
Window-bound writes affect only comparator events that have not yet run.
There is no sub-instruction arbitration or active-display bus contention.
Sprite preparation samples a whole row at once; it does not reproduce individual fetch timing.
Affine origins advance at line boundaries, using the current coefficients rather than per-pixel register timing.
Vertical mosaic counters advance at line ends. Horizontal mosaic still uses screen-derived block boundaries.
Horizontal window history follows the four-cycle comparator model described in [video.md](video.md), not a complete LCD pixel pipeline.
Vertical window flags update at scanline starts, independently of row capture.
These rules support raster palette, page, scroll, window, and effect changes, but not every hardware raster effect.
The raster demo copies a 256-halfword original color table to `0x02001000` with BIOS `CpuSet`.
During each VBlank, its ARM code polls input, resets the row-zero color, and rearms DMA0.
DMA0 copies one halfword to palette entry zero at each visible HBlank, with repeat and fixed destination enabled.
The source starts at the next row's color. The CPU waits through the visible frame using `VBlankIntrWait` and HALT.
Two debug words at `0x02000000` contain the update count and five-bit color-band phase.
The host neither writes palette colors nor uses those debug words to drive execution.

56
docs/hardware/dma.md Normal file
View file

@ -0,0 +1,56 @@
# Direct memory access (DMA)
Four DMA channels occupy `0x040000b0..0x040000df`. Each channel has a 12-byte register block:
| Offset | Register | Behavior |
| --- | --- | --- |
| `+0` | SAD | Write-only initial source address |
| `+4` | DAD | Write-only initial destination address |
| `+8` | CNT_L | Write-only halfword/word count |
| `+10` | CNT_H | Read/write address controls, repeat, width, timing, IRQ, and enable |
Byte, halfword, and word register writes are supported. A combined count/control write sets the count before enabling DMA.
Control masks are `0xf7e0` for DMA0–2 and `0xffe0` for DMA3.
Source addresses use 27 bits on DMA0 and 28 bits on DMA1–3.
Destination addresses use 27 bits on DMA0–2 and 28 bits on DMA3.
Initial addresses align down to the selected halfword or word width.
DMA0–2 use 14-bit counts; DMA3 uses 16-bit counts. Zero means 16,384 or 65,536 data units, respectively.
Write-only register reads return zero as a placeholder, not hardware open-bus behavior.
A rising enable bit copies the programmed addresses and count into internal transfer state.
Register writes alone do not move data. `Machine::step()` services one halfword or word at a time.
Source modes are increment, decrement, or fixed. Destination modes also include increment/reload.
Game Pak source addresses increment regardless of the selected source mode.
Writes to programmed addresses/count do not alter an active block's internal pointers or remaining count.
Disable the channel before changing its width, address modes, or start timing; live reconfiguration is not hardware-validated.
Supported triggers:
- Immediate: ready after the enabling instruction; repeat does not keep it running.
- VBlank: ready on entry to line 160.
- HBlank: ready at HBlank entry on visible lines 0–159 only.
Blanking triggers do not depend on DISPSTAT IRQ enables or forced blank.
Enabling during an existing blank period waits for the next edge.
A busy channel ignores additional edges. Pending requests are not a queue.
On repeated blanking transfers, completion reloads the programmed count and optionally the destination.
The source continues from its current position. Non-repeated completion clears the enable bit.
Clearing enable manually cancels a waiting or active transfer.
DMA0 has highest priority, followed by DMA1, DMA2, and DMA3.
A higher-priority request can interrupt a lower-priority transfer between data units.
The CPU remains paused while any channel is ready; display and timer clocks continue.
Completion with local IRQ enable latches IF bits 8–11, independently of IE, IME, and CPSR.I.
CPU delivery waits until no DMA is ready. IRQ entry does not acknowledge IF.
Timing uses independent source and destination bus costs, including WAITCNT and 128 KiB ROM boundaries.
The first unit uses non-sequential accesses plus two internal cycles. Later units use sequential accesses.
Each step reports zero code cycles. Overlapping transfers read each unit after the preceding write; they are not bulk host copies.
Startup scheduling delays, channel-resumption costs, CPU fetch-state effects, and display-bus contention remain unmodeled.
Special timing modes (sound FIFO/video capture), Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
DMA reads below work RAM, including BIOS reads, return `DmaError::UnsupportedSource` rather than exposing BIOS bytes.
DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifying transfers are not supported.
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
Unmapped memory and cartridge writes remain diagnostics, not hardware open-bus/latch behavior.
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.

110
docs/hardware/timers-irq.md Normal file
View file

@ -0,0 +1,110 @@
# Timers, interrupts, and HALT
## Timers, interrupt registers, and the machine clock
`Machine::new(cpu, memory)` combines the CPU and memory bus.
`Machine::step()` first services one ready DMA data unit, keeping the CPU paused.
If HALT is waiting and no DMA is ready, it advances device clocks to the next event without executing CPU code.
Otherwise, it samples the IRQ line, including the CPU's interrupt mask, then enters IRQ mode or executes one instruction.
Each successful step advances display and timer clocks by its nominal cost.
`Machine::last_timing()` reports separate code, data, internal, and idle cycle totals for the last successful step.
`StepTiming::idle_cycles` counts device-clock cycles during HALT; it does not count CPU work.
IRQ entry is a separate step; the vector instruction runs on the following step.
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
Bus writes take effect before the step's device-clock update.
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
The other step kinds remain `Instruction` and `IrqEntry`.
`MachineError::Cpu` and `MachineError::Dma` identify the source of a diagnostic.
Failed steps do not advance the clock or partially change CPU or device state. Earlier successful steps remain committed.
**The clock now uses instruction-specific costs, but it is not cycle-accurate.**
Device clocks update in batches after instructions, exception entries, DMA units, or HALT idle intervals.
Reads observe register state before that batch.
Writes, including timer start/stop and IF acknowledgement, take effect before the entire batch.
This does not reproduce bus-access timing within an instruction or IRQ synchronization delays.
`Cpu::step` remains a CPU-only API. It does not honor HALT, execute DMA, advance device clocks, or sample device IRQs.
`Cpu::step_timed` executes the same operation and returns `StepTiming`, without advancing devices.
`Machine::step` uses that result to advance device clocks. Failed steps preserve the previous timing result.
`Memory::advance_cycles(n)` advances device clocks, latches DMA requests, and updates HALT wake-up state.
It does not execute CPU instructions or DMA units.
Multiple unserviced DMA requests coalesce; bulk clock advances do not replay every missed transfer.
Reads and writes alone consume no cycles.
`Memory::cycles()` and `Machine::cycles()` report the supplied cycle total as a wrapping 64-bit counter.
Mapped input/output (I/O) registers:
| Address | Register | Behavior |
| --- | --- | --- |
| `0x04000100` + `4*n` | Timer n counter/reload | Reads the counter; writes the separate reload value |
| `0x04000102` + `4*n` | Timer n control | Prescaler, count-up, local IRQ enable, and start/stop |
| `0x04000200` | IE | Enables selected interrupt sources; bits 0–13 are writable |
| `0x04000202` | IF | Latches requests; writing 1 clears the corresponding bit |
| `0x04000204` | WAITCNT | Configures first/second ROM access wait states; unused bits read as zero |
| `0x04000208` | IME | Bit zero gates IRQ delivery; other bits read as zero |
| `0x04000300` | POSTFLG | Bit-zero post-boot flag; CPU writes require BIOS execution |
| `0x04000301` | HALTCNT | Write-only low-power control; HALT supported, STOP returns a diagnostic |
Timers count at supplied clock rates divided by 1, 64, 256, or 1024.
Timers 1–3 can instead count the preceding timer's overflows. Timer 0 ignores count-up selection.
Starting a stopped timer copies reload into the counter. Overflow also reloads the counter.
Writing reload while running leaves the counter unchanged until overflow or restart.
A combined 32-bit reload/control write uses the new reload value when starting the timer.
Stopping a timer freezes its counter. Restarting it reloads the counter.
Bulk clock advances preserve all overflow pulses through cascaded timers without looping once per cycle.
Overflow latches IF only when the timer's local IRQ-enable bit is set.
IE, IME, and CPSR.I gate delivery, not the pending flag.
Entering IRQ mode does not clear IF. Software must acknowledge the request.
Unused register bits read as zero. Unknown I/O addresses and mirrors remain unmapped.
Byte, halfword, and word accesses are supported for this register subset.
Timer startup delays and shared prescaler phase are not modeled.
The provisional timer model resets its private prescaler phase on enable or clock-source changes.
Audio events, STOP, and interrupt sources other than timers, display events, and DMA completion are not implemented.
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
The handler stops Timer 0, acknowledges IF, increments r10, and returns with `SUBS pc, lr, #4`.
The demo ends after 21 steps and 153 nominal cycles with one handler call, using default WAITCNT settings.
These counts test the timing model; they are not hardware timing measurements.
Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention.
## HALT and power-control registers
Writing HALTCNT with bit 7 clear requests HALT. Bits 0–6 are ignored.
The current instruction finishes before the machine pauses CPU execution.
Timers, display timing, and DMA continue to run. HALT does not pause the native window or host input.
`Memory::halted()` and `Machine::halted()` report whether HALT is still waiting.
HALT ends when `IE & IF` becomes nonzero, even when IME or CPSR.I blocks IRQ delivery.
An already pending enabled request prevents the CPU from sleeping.
Wake-up does not acknowledge IF. Clearing IF after wake-up does not put the CPU back into HALT.
On the next machine step, ready DMA still takes priority.
The CPU then enters an unmasked IRQ handler or resumes the next instruction.
Each `HaltIdle` step advances to the earliest display edge or independently clocked timer overflow.
Display edges include every HBlank entry and scanline start. Cascaded timers receive pulses at their predecessor's overflow.
These bounds prevent idle advances from skipping DMA triggers or wake-up events.
They also keep frame execution bounded when no interrupt can wake the CPU.
Idle steps neither fetch instructions nor change CPU registers; code, data, and internal cycle counts remain zero.
Exact hardware entry/exit delays and pipeline effects are not modeled.
CPU writes to POSTFLG and HALTCNT take effect only when the executing instruction address is within the 16 KiB BIOS region.
ARM and Thumb use the same rule. This is an instruction-address check, not a simulated prefetched PC signal.
CPU writes from RAM or cartridge ROM are ignored, including STOP requests.
Bare `Memory::write*` calls remain host/debug setup and can write these registers without a CPU access context.
DMA writes to the power-control block return `DmaError::PowerControlDestination`; this hardware edge case is not modeled.
POSTFLG resets to zero and stores only bit 0. Setting it does not perform a BIOS boot or initialize RAM.
HALTCNT reads return zero as a placeholder, not hardware open-bus behavior.
The reserved bytes at `0x04000302..0x04000303` read zero and ignore writes.
Byte, halfword, and word writes are supported; a halfword/word at POSTFLG also writes HALTCNT.
STOP requests from BIOS code or host setup return `MemoryError::UnsupportedStop` before any write commits.
This prevents STOP from silently behaving like HALT while its clock-gating and wake sources are absent.
Multi-byte and block-store diagnostics preserve POSTFLG, HALT state, CPU registers, and the current step's clock.
CPU-only stepping can record a HALT request but does not enforce the pause. Use `Machine::step()` for device integration.
The graphics demo now uses the optional original BIOS replacement's VBlankIntrWait service.
Its IRQ callback acknowledges VBlank and updates the BIOS RAM flag before the service returns.

420
docs/hardware/video.md Normal file
View file

@ -0,0 +1,420 @@
# Video: backgrounds, sprites, windows, and effects
Rendering behavior implemented in `crates/gba-core/src/video/`, plus button input registers.
## Graphics and button input
`DISPCNT` at `0x04000000` selects the display mode. The CGB mode bit is forced to zero.
`GREENSWAP` at `0x04000002` exchanges the green channels of adjacent pixel pairs when bit zero is set.
`KEYINPUT` at `0x04000130` exposes ten active-low buttons. Unused bits read as zero; writes are ignored.
`Memory::set_buttons(Buttons)` supplies pressed-high host input. The I/O layer converts it to active-low register bits.
`KEYCNT` and keypad IRQs are not implemented; accesses to KEYCNT return unmapped-memory diagnostics.
Video RAM starts at `0x06000000` and contains 96 KiB.
Its 128 KiB mirror layout repeats the final 32 KiB in offsets `0x18000..0x1ffff`.
Palette RAM starts at `0x05000000` and repeats every 1 KiB.
Halfword and word writes preserve little-endian order.
Byte writes to palette or background video RAM duplicate the byte into the addressed halfword.
Byte writes to object video RAM are ignored. Its start is `0x14000` in bitmap modes and `0x10000` in tile modes.
Object attribute memory (OAM) starts at `0x07000000` and repeats every 1 KiB across the `0x07` region.
OAM supports byte reads and halfword/word reads and writes. Byte writes do nothing.
Its 128 eight-byte entries hold three sprite attributes and one affine-parameter halfword each.
All bytes start at zero. Software must disable unused sprites; zeroed entries describe active 8×8 sprites.
CPU and DMA accesses share this memory. Active-display access restrictions are not modeled.
`Memory::render_frame` creates a snapshot without advancing the emulated clock.
Mode 3 stores 38,400 RGB555 pixels at `0x06000000..0x06012bff` in row-major order.
The renderer samples that bitmap through BG2's matrix and reference-point registers.
Pixel bit 15 and the DISPCNT page-select bit do not affect Mode 3 output. Black pixels are opaque.
Disabling BG2 leaves palette entry zero behind sprites. Forced blank displays white, regardless of the selected mode.
All bitmap modes support regular and affine sprites, windows, and color effects.
Enabling backgrounds other than BG2 in bitmap modes produces errors without changing the output buffer.
Reserved display modes 6 and 7 return errors. Modes 0 through 5 support snapshots and row capture, not complete hardware implementations.
Mode 4 stores 240×160 byte-sized palette indices. Index zero is transparent and can reveal a lower-priority sprite.
A nonzero palette index remains opaque even when its palette color is black.
Palette changes affect the next captured row or debug snapshot, not rows already captured.
Mode 5 stores 160×128 RGB555 pixels. All in-bounds pixels are opaque, including black. Pixel bit 15 is ignored.
Mode 5 does not automatically stretch to the LCD size. BG2's affine matrix can scale or position the bitmap.
Modes 4 and 5 select their visible page with DISPCNT bit 4:
| Mode | Page 0 pixel data | Page 1 pixel data |
| --- | --- | --- |
| 4 | `0x06000000..0x060095ff` | `0x0600a000..0x060135ff` |
| 5 | `0x06000000..0x06009fff` | `0x0600a000..0x06013fff` |
Both page bases are 40 KiB apart. Mode 4's padding after each image is not pixel data.
Writes to the hidden page do not change visible pixels until that page is selected.
Mode 4 byte writes still duplicate the byte into both halves of a halfword, as required by the video memory bus.
Palette data and sprite tiles are shared between pages. Bitmap-mode sprite tiles start at `0x06014000`.
Mode 0 supports all four text backgrounds, with these features:
- `BG0CNT..BG3CNT` at `0x04000008..0x0400000f`: priority, character base, palette depth, map base, and size.
- Horizontal/vertical scroll latches at `0x04000010..0x0400001f`, with byte-write merging and nine-bit offsets.
- Scroll registers are write-only. Reads return zero as a placeholder; open bus is not modeled.
- Control masks are `0xdfcf` for BG0/BG1 and `0xffcf` for BG2/BG3.
- 8×8 tiles with 4-bit palette indices and sixteen banks, or 8-bit indices with one palette.
- Horizontal and vertical tile flips, and ten-bit tile numbers.
- 256×256, 512×256, 256×512, and 512×512 maps assembled from 32×32-tile screen blocks.
- Scroll coordinates wrap at map edges. The affine overflow bit does not change text backgrounds.
- Index zero is transparent in every palette bank. Nonzero indices can display opaque black.
- Lower priority numbers win. Equal priorities select BG0 before BG1, BG2, and BG3.
- Uncovered pixels use palette entry zero. Green swap runs after composition.
Mode 0 supports mosaic independently on each background. Rendering errors leave the output buffer unchanged.
Background tile or map fetches outside the first 64 KiB return diagnostics.
This is not a complete picture processing unit (PPU).
Affine background snapshots support these configurations:
- Mode 1: text BG0/BG1 and affine BG2.
- Mode 2: affine BG2/BG3.
- Modes 3, 4, and 5: affine bitmap BG2, with out-of-bitmap samples always transparent.
- Enabling a background unavailable in the selected mode returns a diagnostic rather than silently ignoring it.
`BG2PA..BG2PD` occupy `0x04000020..0x04000027`. BG2 reference points X/Y occupy `0x04000028..0x0400002f`.
BG3 uses the corresponding registers at `0x04000030..0x0400003f`.
Coefficients are signed 16-bit values divided by 256. Reference points are signed 28-bit values divided by 256.
All registers are write-only; reads return zero as an open-bus placeholder.
Byte and halfword writes merge into the stored values. Reference-point bits 28–31 are ignored.
Registers reset to zero, not an identity matrix. Software must initialize PA and PD to 256 for identity sampling.
The existing Mode 3 demo now performs that initialization through ARM stores.
The snapshot source coordinates are `X + PA*x + PB*y` and `Y + PC*x + PD*y`, with fixed-point scaling.
The renderer floors fractional coordinates after summing, including negative coordinates.
Mode 0 text backgrounds ignore these affine registers. Affine backgrounds ignore text scroll registers.
Affine tile backgrounds use flat maps of byte-sized tile indices, without text-map flips or palette-bank attributes.
Every tile is 8×8 with eight-bit palette indices, regardless of BGCNT bit 7.
Map sizes are 128×128, 256×256, 512×512, or 1024×1024 pixels.
BGCNT bit 13 selects wrapping or transparent edges in Modes 1 and 2. All bitmap modes ignore this bit.
Palette index zero is transparent in tiled backgrounds and Mode 4. Mode 3/5 black bitmap pixels remain opaque.
Background priorities, sprite composition, and green swap apply after sampling.
Map fetches beyond the supported 64 KiB background area still return diagnostics.
**Timing limit:** debug snapshots use one set of programmed registers for the whole image.
Row capture instead samples from separate internal X/Y origins.
At each visible line end, enabled affine backgrounds add the current PB/PD to their origins.
HBlank DMA coefficient changes therefore affect the next row's increment, not all preceding rows.
BG2 tracks in Modes 1–5; BG3 tracks in Mode 2. Disabled or unavailable backgrounds do not accumulate.
Window masking does not pause tracking. This row-level model also continues tracking during forced blank.
X/Y writes before HBlank replace the current row's origin for the written axis.
Writes after row capture set the next row's origin and override that axis's next increment.
Partial writes merge into programmed values, not accumulated origins. Matrix writes do not reload origins.
Both axes wrap within signed 28-bit fixed-point storage.
The next visible frame reloads programmed origins at the line-zero boundary, including values written during VBlank.
Debug snapshots continue to use programmed origins and absolute screen Y.
Vertical background mosaic holds the origin until the shared background counter becomes zero at a line end.
It then adds the current PB/PD multiplied by the current block height.
Changing the size does not reset the counter or recompute the origin from absolute screen Y.
Tracking continues with capture disabled. Fixed-register clock batches remain constant-time.
These are explicit row-level rules, not verified hardware latch timing.
Exact VBlank reload timing, enable delays, forced-blank restart delays, individual sprite fetches, and background pixel-fetch timing remain unmodeled.
Sprites are enabled with DISPCNT bit 12 in any supported display mode:
- All twelve square, horizontal, and vertical size combinations are supported, from 8×8 to 64×64.
- X coordinates wrap at 512; Y coordinates wrap at 256. Rendering clips to the 240×160 screen.
- Regular-sprite horizontal and vertical flips reverse the whole sprite, including multi-tile sprites.
- Four-bit sprites use sixteen OBJ palette banks. Eight-bit sprites use the complete OBJ palette.
- OBJ palette data starts at `0x05000200`, separate from background palettes.
- Index zero is transparent. A nonzero index can display opaque black.
- DISPCNT bit 6 selects consecutive 1D tile rows or fixed-width 2D tile rows.
- Tile numbers count 32-byte slots from `0x06010000`. Eight-bit tiles occupy two slots.
- In 2D mapping, eight-bit base tile numbers ignore bit zero. In 1D mapping, odd bases remain valid.
- 2D columns wrap within 32 slots. Tile addressing wraps within the 32 KiB OBJ area.
- Modes 3–5 suppress sprites whose starting tile number is below 512, regardless of the displayed page.
- Lower priority numbers win object conflicts. Equal priorities retain the earlier OAM entry's color.
- Transparent texels can change priority and mosaic metadata without replacing the stored color or semi-transparency flag.
- The selected sprite beats a background at equal priority. A background with a lower priority number covers it.
- Disabled regular sprites are skipped, regardless of their other attributes.
Affine sprites use a two-by-two matrix for rotation, scaling, reflection, or shear:
- Attribute 0 bit 8 enables affine sampling. Bit 9 then doubles the drawing area's width and height instead of disabling the sprite.
- Attribute 1 bits 9–13 select one of 32 matrices. These bits no longer control horizontal or vertical flips.
- Matrix coefficients PA, PB, PC, and PD occupy OAM offsets `6`, `14`, `22`, and `30`, plus `32 × matrix index`.
- Each coefficient is a signed 16-bit value divided by 256, also called signed 8.8 fixed point.
- `[256, 0, 0, 256]` gives an identity transform. `[128, 0, 0, 128]` enlarges the image 2×.
- The renderer maps drawing-area coordinates back into the source texture, around their respective centers.
- Fractional coordinates round down after both matrix products are added. Out-of-texture samples are transparent.
- Double-size mode enlarges only the drawing area, not the source texture or the matrix scale.
- X/Y still locate the drawing area's upper-left corner. Software must adjust X/Y to preserve the center when changing area size.
- Sprites can share matrices, including matrices beside disabled sprite entries. Updates affect later preparation events or immediate debug snapshots.
- Zero matrices sample the center texel throughout the drawing area. Singular matrices are valid; no matrix inversion occurs.
Semi-transparent sprites and OBJ windows support both regular and affine sampling.
Prohibited modes/shapes return explicit diagnostics.
Unsupported active sprites are checked even when offscreen. Global OBJ disable skips these checks.
Sprite preparation applies a nominal per-row work allowance. Individual fetch timing and OAM access contention remain unimplemented.
Sprite priority includes transparent-texel metadata updates, but the core has not run public hardware test ROMs.
### Buffered sprite row preparation
The core prepares the following visible sprite row at cycle40 of lines0–158 and line227.
Line227 prepares row0. Lines159–226 do not prepare another visible row.
This is a row-at-once approximation of the hardware's one-line-ahead engine, not an accurate sequence of OAM/VRAM fetches.
Preparation samples OAM attributes, affine matrices, sprite VRAM, tile mapping, bitmap restrictions, and the ahead-of-display OBJ mosaic phase.
It also samples DISPCNT bit 5 to select the row's nominal sprite work allowance.
It stores palette indices, priority, mosaic and semi-transparency metadata, and OBJ-window coverage.
The following row's HBlank composition resolves those indices through the current palette.
Horizontal OBJ mosaic uses the current width. Window masks, blending coefficients, global display enables, and color effects also remain current.
A write before the preceding line's cycle40 can affect preparation. A write after that event cannot change the prepared row.
Under this boundary policy, a host write at cycle40 is already too late.
CPU/DMA writes commit before their nominal clock batches, so a write crossing cycle40 participates in preparation.
For example, an OAM write in row0 HBlank affects row2 or later, not the already-prepared row1.
Global OBJ disable makes preparation empty. Disabling OBJ before composition hides an already-prepared row.
OBJ-window coverage is prepared regardless of its display-enable bit; composition requires both OBJ and OBJ-window enables.
Forced blank does not stop preparation in this model. Hardware restart delays remain unmodeled.
Reset starts at visible row0 rather than a hardware boot phase.
The first positive clock advance therefore prepares row0 from host setup, using mosaic phase0.
Later row0 preparation uses line227. Zero-cycle advances do not initialize or refresh the buffers.
Preparation continues with capture disabled. Capture toggles and debug snapshots do not reset prepared state.
Preparation errors are retained for the target row, not returned by CPU/DMA clock advancement.
Visible composition reports them through the existing deferred frame diagnostic.
Forced blank and global OBJ disable suppress unused sprite diagnostics. A later valid frame restores presentation.
Individual fetch timing and video-bus arbitration remain unmodeled.
### Nominal sprite work allowance
Each prepared row receives 1,210 nominal sprite-rendering cycles, or 954 when DISPCNT bit 5 (`HBlank Interval Free`) is set.
Debug snapshots apply the same allowance independently to each row, using current registers.
This is an aggregate rendering limit. It does not advance CPU or display clocks.
Objects consume work in OAM order, regardless of display priority:
- An active regular sprite costs its drawing-canvas width.
- An active affine sprite costs `10 + 2 × drawing-canvas width`.
- Double-size affine mode charges the enlarged canvas, not the source texture width.
- These active costs include inspection. Disabled, vertically excluded, and entirely horizontally offscreen entries cost two inspection cycles instead.
- A sprite that exceeds the remaining allowance prepares only a left-to-right canvas prefix. Later objects produce no samples.
- An incomplete affine setup or pixel consumes the remaining allowance. A later regular sprite cannot reuse it.
- There is no separate 32-object-per-row limit.
Transparency, occlusion, window masks, mosaic, out-of-texture affine samples, and bitmap-restricted tile numbers do not refund work.
Partially clipped sprites charge their full canvas width, including clipped columns. Hardware left-clipping savings are not modeled.
Invalid active OAM entries still produce development diagnostics after allowance exhaustion, preserving the existing validation policy.
DISPCNT bit 5 is sampled during whole-row preparation, not during later composition.
Changing it after the preceding line's cycle40 therefore cannot change that prepared row's allowance.
The reduced allowance does not implement HBlank OAM access arbitration.
These nominal costs combine documented limits with emulator-reference inspection costs; exact hardware cutoffs remain unverified.
### Mosaic snapshots and vertical counters
`MOSAIC` at `0x0400004c` is write-only. Reads return zero as an open-bus placeholder.
Byte writes merge into its 16-bit latch. The unused upper halfword at `0x0400004e` reads zero and ignores writes.
CPU and DMA halfword/word writes use the same path.
| Bits | Dimension, stored as size minus one |
| --- | --- |
| 0–3 | Background horizontal size |
| 4–7 | Background vertical size |
| 8–11 | Sprite horizontal size |
| 12–15 | Sprite vertical size |
Each dimension ranges from one to sixteen pixels. Static zero fields give ordinary 1×1 sampling.
A mid-frame write of zero preserves any existing vertical phase until the counter next resets.
BGCNT bit 6 enables mosaic for an individual background. OBJ attribute 0 bit 12 enables sprite mosaic.
The register alone does not enable a layer's mosaic effect.
Debug snapshots align background blocks to the screen origin, not tile boundaries or scroll offsets.
Their upper-left samples enter text scrolling or affine mapping before palette lookup and composition.
Captured rows instead use live vertical phase; their horizontal blocks remain screen-aligned.
This applies to Modes 0–5, including both Mode 4/5 pages, transparent indices, and transformed bitmap edges.
Window masks use the actual output coordinate, so a window can cut through a mosaic block.
Sprite snapshots use the screen's mosaic row. Prepared sprite rows subtract the latched ahead-of-display counter from the local row.
Both paths clamp a partial first block to the sprite's first row.
Flips and affine transforms follow that row selection. Vertical mosaic does not enlarge the drawing area.
Horizontal sprite mosaic latches the selected object sample after object priority resolution.
The latch updates at a block boundary, at transitions to or from non-mosaic samples, or for a lower priority number.
Transparent samples participate through their priority and mosaic flags. Each scanline starts with an empty latch.
The latch runs before window masking and alpha blending; this is not a final-frame pixelation filter.
OBJ-window coverage ignores both mosaic dimensions. Transparent window texels can still affect object metadata.
Captured rows use one four-bit vertical counter for all backgrounds and another for sprites.
The background counter follows displayed rows. The OBJ counter follows the row being prepared, one row ahead.
At each applicable line end, a counter increments by one.
If the incremented value equals the current height, the counter resets to zero. Otherwise it wraps to four bits.
The background counter resets at entry to row160. The OBJ counter resets one displayed line earlier and stays zero through line227.
At frame wrap, OBJ advances to the phase for preparing row1; row0 has already retained phase0.
Size writes preserve the current counters. Shrinking below the current phase can delay the next block until four-bit wraparound.
For example, changing height8 to height2 at phase6 produces phases7 through15 before zero.
Text backgrounds then sample `screen_y - background_counter`, followed by current scroll and tile lookup.
Affine backgrounds instead retain their origin and add `height * PB/PD` when the counter becomes zero.
After that example's delayed reset, an affine origin advances by two rows, not sixteen.
Layer enable bits, per-layer mosaic flags, forced blank, and host capture enable do not pause or reset these counters.
Mosaic-disabled layers ignore the counters. OBJ-window coverage also ignores them.
Background VRAM samples at capture; sprite OAM/VRAM samples during preparation. Palette lookup remains current at composition.
Vertical mosaic does not copy a cached row of final RGB pixels.
Snapshots, presentation, failed batched writes, and zero-cycle advances do not change counter state.
Fixed-register clock batches remain constant-time, including frame wrap.
**Timing limit:** counter updates use line boundaries, and sprite preparation samples a whole row at cycle40.
Individual sprite fetches, per-access size latching, and horizontal counter timing remain unmodeled.
The selected counter rules follow reference implementations. They have not been verified here against GBA hardware test programs.
The mosaic demo shares the tile scene's ARM program builder and writes MOSAIC during each VBlank update.
### Window masks and color effects
| Registers | Addresses | Access and stored masks |
| --- | --- | --- |
| WIN0H, WIN1H | `0x04000040..0x04000043` | Write-only, eight-bit horizontal bounds |
| WIN0V, WIN1V | `0x04000044..0x04000047` | Write-only, eight-bit vertical bounds |
| WININ, WINOUT | `0x04000048..0x0400004b` | Read/write, `0x3f3f` |
| BLDCNT | `0x04000050..0x04000051` | Read/write, `0x3fff` |
| BLDALPHA | `0x04000052..0x04000053` | Read/write, `0x1f1f` |
| BLDY | `0x04000054..0x04000055` | Write-only, `0x001f` |
Byte writes merge into the stored halfwords. CPU and DMA writes use the same register path.
Write-only reads return zero, not hardware open-bus values. Unused bytes at `0x04000056..0x04000057` read zero and ignore writes.
MOSAIC at `0x0400004c` and its unused upper halfword are described above.
DISPCNT bits 13–15 enable WIN0, WIN1, and OBJ windows.
When no window is enabled, every globally enabled layer can display and color effects are permitted.
Otherwise WIN0 takes precedence over WIN1, then OBJ window, then the outside region.
Masks are selected by region; overlapping windows do not combine their masks.
Each region independently enables BG0–BG3, OBJ, and effects. A window cannot enable a globally disabled layer.
The palette backdrop remains visible when every layer is masked out.
Bounds store the start in the high byte and the excluded end in the low byte.
Debug snapshots use wrapping eight-bit intervals. Equal bounds select an empty interval.
Captured rows instead use persistent vertical flags for WIN0 and WIN1:
- At each scanline start, matching the top edge sets the flag; matching the bottom edge clears it.
- The bottom edge wins when both bounds match the same scanline.
- With no matching edge, the flag keeps its previous value, including across frame boundaries.
- Hidden rows 160–227 run the same comparisons. VCOUNT never reaches bounds 228–255.
- WIN0V/WIN1V writes change future comparisons. They do not replay an edge from an already-entered row.
- Window enable bits and forced blank affect composition, not vertical tracking.
Moving the top behind VCOUNT does not open an inactive window.
Moving the bottom behind VCOUNT can leave an active window open until a later matching edge.
Equal unreachable bounds can retain an active flag even though the debug snapshot shows an empty interval.
A wrapping window can differ on the first frame because its top edge has not occurred yet.
Both flags start clear. Host setup before the first positive clock advance participates in the initial line-zero comparison.
After that startup step, even a write at cycle zero cannot repeat that line's comparison.
Zero-cycle advances, snapshots, and capture toggles do not update the flags.
Tracking also runs with capture disabled. Fixed-register bulk advancement remains constant-time.
Captured rows also retain horizontal comparator history for each window:
- X runs from 0 through 255, with comparisons at line cycles 0, 4, …, 1020.
- Matching the left edge sets the flag. Matching the right edge clears it; right wins when both edges match.
- Visible columns 0–239 retain their flags for later row composition. Offscreen columns still update the persistent flag.
- Flags carry across line and frame boundaries. Hidden lines, disabled windows, and forced blank do not stop tracking.
- WIN0H/WIN1H writes affect future comparisons, not already-recorded columns.
Advancing clocks consumes the half-open interval from the current cycle to the ending cycle.
A write at cycle `4*x` therefore precedes X's comparison; a write at `4*x+1` is too late.
A zero-cycle advance does not perform a comparison.
Moving an edge behind the current column can miss it and retain the old flag into the following line.
Wrapping horizontal bounds can differ on the first row because no previous line has set the flag.
HBlank starts at cycle1006, after all visible columns have been recorded.
Columns252–255 occur later, so HBlank DMA can still set or cancel an offscreen edge before the next line.
Already-captured rows remain unchanged. The horizontal history records window coverage, not pixel colors.
Region masks, enable bits, effects, palette, and background VRAM remain current-row inputs sampled at HBlank.
Sprite OAM/VRAM and OBJ-window coverage come from the preceding line's preparation.
CPU/DMA writes still commit before their nominal cycle batch. Per-access bus timing and the LCD merge pipeline remain unimplemented.
This comparator model follows a reference implementation; it has not been verified here against GBA hardware test programs.
Horizontal history is retained even with capture disabled. Capture toggles and debug snapshots do not reset it.
Bulk clock advancement processes at most three line segments, regardless of the number of crossed frames.
This preserves constant-time advancement relative to emulated cycles, with fixed 240-column history buffers.
OBJ window coverage comes from nonzero sprite texels, regardless of their palette colors or background priorities.
Window sprites contribute no color pixels. Both DISPCNT bits 12 and 15 must be set for OBJ window coverage.
Normal sprites do not occlude window coverage, and the regional OBJ display bit does not disable the window mask.
The renderer selects the two frontmost unmasked, non-transparent layers before applying effects.
Alpha blending requires the top layer to be a first target and the immediate lower layer to be a second target.
A non-target layer between them prevents blending; the renderer does not search farther back for a target.
The backdrop can be a target but cannot blend with itself. Only one OBJ color pixel participates, so OBJ-to-OBJ blending is unavailable.
Coefficients are stored as five-bit values and clamped to 16 when used.
Each RGB555 channel uses `min(31, (first*EVA + second*EVB) / 16)`, with integer division after summing.
Brightness uses `color + (31-color)*EVY/16` or `color - color*EVY/16` on selected top-layer channels.
No layer receives brightness changes before it becomes a blend target.
Semi-transparent OBJ pixels request alpha blending regardless of BLDCNT's mode and first-target selection.
They still require an eligible second target and the region's effect-enable bit.
Without a second target, a selected brightness effect can apply instead.
Green swap runs after color effects. Forced blank bypasses all composition and effects.
The effects demo reuses the tile scene and ARM instruction builder.
Its CPU writes a 112×80 WIN0 rectangle, disables effects inside it, and enables effects outside it.
BLDY and both alpha coefficients are eight. Z selects BG0/OBJ as first targets and BG1 as the second target.
Three debug words at `0x02000000` retain the update count and nine-bit horizontal/vertical scroll.
The demo updates registers during VBlank. Vertical window state still depends on edges reached in previous rows and frames.
Moving its bottom beyond row227 can keep the unchanged-color region active at the next frame's top.
The smoke test tracks this state instead of assuming a geometric rectangle on every frame.
### CPU-driven tile and background demos
The tile demo copies 1 KiB palette and OAM images with `CpuSet`, and 96 KiB video data with `CpuFastSet`.
Its ARM program configures a 4-bit foreground, an 8-bit 512×512 terrain background, and one 16×16 sprite.
Unused sprite entries are explicitly disabled.
It installs a BIOS IRQ callback and uses `VBlankIntrWait` before updating scroll registers and OAM.
Z sets the regular sprite's horizontal-flip bit. X changes its background priority from zero to one.
Q and W select affine double-size mode and write matrix zero through real ARM halfword stores.
Q uses `[181, 181, -181, 181]` for a quantized 45-degree clockwise rotation.
W halves the coefficients. With Q held, the matrix becomes `[90, 90, -90, 90]`.
The demo moves the drawing-area origin from `(112, 72)` to `(104, 64)` to preserve the center at `(120, 80)`.
Releasing Q and W restores regular mode. Z has no effect while affine mode is active.
Opposite directions cancel. Enter resets both offsets. Offsets wrap to nine bits.
Three debug words at `0x02000000` hold the update count, horizontal scroll, and vertical scroll.
The tile, effects, mosaic, raster, bitmap, and affine background demos share row capture, bounded frame execution, and keyboard handling.
The Mode 4/5 and affine raster demos reuse the affine background demo's ARM instruction builder.
The affine raster program builds 160 halfwords at `0x02001000` during each VBlank.
It rearms DMA0 to copy one halfword to BG2PB at every visible HBlank.
Sixteen positive offsets alternate with sixteen negative offsets. Z uses the base transform without distortion.
The CPU writes all assets, tables, registers, and DMA configuration. The host only supplies buttons and presents captured frames.
Their CPU copies a 512-byte palette with `CpuSet` and an 80 KiB two-page video image with `CpuFastSet`.
During each VBlank update, ARM code stores the transform and selects the page from bit 5 of its update counter.
Z overrides that selection with page 1. Three debug words at `0x02000000` hold the update count and signed pan X/Y.
The host does not use the counter as a breakpoint or write DISPCNT.
The affine background demo uses Mode 2 BG2 with a 256×256 map and four original eight-bit tiles.
Its CPU copies a 512-byte palette with `CpuSet` and a 64 KiB video image with `CpuFastSet`.
Each update stores the matrix and computes the reference point as `(map center + pan)*256 - matrix*(screen center)`.
Q uses `[181, 181, -181, 181]`; W halves the coefficients. Z clears BGCNT's overflow bit.
The CPU uses `VBlankIntrWait` before each update. Three debug words at `0x02000000` hold update count and signed pan X/Y.
The graphics demo's ARM code initializes DISPCNT and configures DMA3 to fill all 38,400 background pixels under forced blank.
DMA3 reads a fixed halfword at `0x02000010`; the CPU supplies its color and waits while DMA fills video RAM.
ARM code then polls KEYINPUT and erases/redraws the square.
It clamps movement to the screen. Opposite directions cancel, including at screen edges.
The host supplies input and renders the result; it does not write the demo's pixels or position.
The ARM program installs a ROM-side IRQ callback and enables VBlank requests in DISPSTAT and IE.
Before each redraw, it invokes `SWI 0x050000` for VBlankIntrWait.
The service discards the old BIOS flag and uses HALT while waiting for the next VBlank request.
After wake-up, IRQ dispatch calls the ROM handler to acknowledge IF and record the BIOS flag.
The service consumes that flag and restores the caller's status before ARM code redraws during VBlank.
It stores an update counter, x, y, and color at four words starting at `0x02000000`, for tests and inspection only.
The host no longer uses this counter as a breakpoint.
The first call runs background initialization under forced blank, waits for Mode 3 to become active, and primes the VBlank wait.
The host then runs between consecutive VBlank-entry events and presents the captured image from the preceding visible period.
A 200,000-machine-step limit bounds startup and frame execution, including DMA units and HALT idle intervals.
Errors retain completed steps but do not replace the output image.
Normal demo calls span one display frame and stop at the exact VBlank edge during HALT.
Other programs can still overshoot by an instruction or DMA-unit cost.
The graphics window limits presentation to approximately 59.73 Hz using the GBA clock ratio.
Host sleeps do not advance emulated time. Slow hosts do not skip emulated frames.
The original host-generated display test keeps its independent 60 Hz cap.