Skip to main content

rustysnes_cart/coproc/
sa1.rs

1//! The SA-1 board — the Super Accelerator system (a second 65C816 + ASIC) on the cartridge.
2//!
3//! SA-1 (Nintendo's "Super Accelerator 1") is the most capable on-cart coprocessor: a second WDC
4//! 65C816 clocked at ~10.74 MHz (twice the S-CPU's fast rate) plus a support ASIC. Unlike the DSP
5//! family there is **no chip-ROM dump** — the SA-1 program lives in the cartridge ROM, so the board
6//! is functional the moment an SA-1 cart loads (`docs/cart.md`, `docs/adr/0003`). Titles: Super
7//! Mario RPG, Kirby Super Star, Kirby's Dream Land 3, Marvelous, SD F-1 Grand Prix, …
8//!
9//! ## Why this board is split across two crates
10//!
11//! The one-directional chip-crate graph forbids `rustysnes-cart` from depending on `rustysnes-cpu`
12//! (`docs/architecture.md`). So this board owns the entire **SA-1 system state** — the $2200–$23FF
13//! register file, the Super-MMC ROM banking, BW-RAM (shared) + I-RAM (2 KiB internal), the
14//! arithmetic unit, the (normal + character-conversion) DMA, the variable-length bit unit, and the
15//! H/V timer — and exposes the SA-1 CPU's *memory view* through the [`crate::board::Board`]
16//! second-CPU hooks. `rustysnes-core` (which already depends on `rustysnes-cpu`) instantiates the
17//! second `rustysnes_cpu::Cpu`, wires a thin bus adapter to these hooks, and steps it in the
18//! scheduler alongside the main CPU. See `docs/scheduler.md` §SA-1.
19//!
20//! ## Memory maps (clean-room from ares `sfc/coprocessor/sa1`, ISC)
21//!
22//! S-CPU (main) view — handled by [`Board::read24`]/[`Board::write24`]:
23//!
24//! | Region (banks : addr)              | Target                                    |
25//! |------------------------------------|-------------------------------------------|
26//! | `$00-$3F,$80-$BF : $2200-$23FF`    | SA-1 registers (S-CPU side)               |
27//! | `$00-$3F,$80-$BF : $3000-$37FF`    | I-RAM (2 KiB)                             |
28//! | `$00-$3F,$80-$BF : $6000-$7FFF`    | BW-RAM (8 KiB block, selected by `$2224`) |
29//! | `$00-$3F,$80-$BF : $8000-$FFFF`    | ROM (Super-MMC blocks C/D)               |
30//! | `$40-$4F : $0000-$FFFF`            | BW-RAM (linear)                          |
31//! | `$C0-$FF : $0000-$FFFF`            | ROM (Super-MMC blocks via `$C/D/E/Fxx`)  |
32//!
33//! SA-1 (second-CPU) view — handled by the second-CPU hooks: the same ROM/BW-RAM/I-RAM/regs plus
34//! the BW-RAM bitmap window (`$60-$6F`, 2/4 bpp) and the linear window (`$40-$5F`).
35
36// Chip-name jargon (SA-1, BW-RAM, I-RAM, MMC, Super-MMC, …) is not Rust code; the four parallel
37// MMC bank fields are deliberately similar names; SA-1 register math narrows widths at well-defined
38// boundaries (matches the bus/exec cast-precision allowances). `if_not_else` / `branches_sharing_
39// code` are allowed so the register/DMA/arithmetic logic stays structurally faithful to the ares
40// reference (clean-room traceability); `missing_const_for_fn` is a nursery suggestion on the pure
41// decode helpers.
42#![allow(
43    clippy::doc_markdown,
44    clippy::similar_names,
45    clippy::cast_possible_truncation,
46    clippy::cast_lossless,
47    clippy::cast_sign_loss,
48    clippy::cast_possible_wrap,
49    clippy::struct_excessive_bools,
50    clippy::too_many_lines,
51    clippy::if_not_else,
52    clippy::branches_sharing_code,
53    clippy::missing_const_for_fn
54)]
55
56use alloc::boxed::Box;
57use alloc::vec;
58
59use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
60
61use crate::board::{Board, Coprocessor, MappedAddr};
62use crate::header::Region;
63
64/// I-RAM size — 2 KiB internal SA-1 RAM.
65const IRAM_SIZE: usize = 0x800;
66/// Default BW-RAM size when the header declares none (SA-1 always has shared work RAM); 256 KiB is
67/// the SA-1 hardware maximum, so a generous default never under-allocates the linear/bitmap views.
68const BWRAM_MAX: usize = 0x4_0000;
69
70/// Fold a linear offset into a `size`-byte image with hardware-accurate mirroring (ares
71/// `Bus::mirror`; the same algorithm as [`crate::board`]'s private `mirror`). Power-of-two sizes
72/// reduce to a mask; the non-power-of-two tail mirrors within itself.
73const fn mirror(mut address: u32, size: u32) -> u32 {
74    if size == 0 {
75        return 0;
76    }
77    let mut base = 0u32;
78    let mut mask = 1u32 << 23;
79    let mut size = size;
80    while address >= size {
81        while address & mask == 0 {
82            mask >>= 1;
83        }
84        address -= mask;
85        if size > mask {
86            size -= mask;
87            base += mask;
88        }
89        mask >>= 1;
90    }
91    base + address
92}
93
94/// The SA-1 $2200–$23FF register file (clean-room from ares `sa1.hpp` `IO`).
95#[derive(Debug, Clone)]
96struct Io {
97    // $2200 CCNT — SA-1 control (written by S-CPU).
98    sa1_irq: bool,
99    sa1_rdyb: bool,
100    sa1_resb: bool,
101    sa1_nmi: bool,
102    smeg: u8,
103    // $2201 SIE / $2202 SIC — S-CPU interrupt enable/clear.
104    cpu_irqen: bool,
105    chdma_irqen: bool,
106    cpu_irqcl: bool,
107    chdma_irqcl: bool,
108    // $2203-$2208 — SA-1 reset/NMI/IRQ vectors.
109    crv: u16,
110    cnv: u16,
111    civ: u16,
112    // $2209 SCNT — S-CPU control (written by SA-1).
113    cpu_irq: bool,
114    cpu_ivsw: bool,
115    cpu_nvsw: bool,
116    cmeg: u8,
117    // $220A CIE / $220B CIC — SA-1 interrupt enable/clear.
118    sa1_irqen: bool,
119    timer_irqen: bool,
120    dma_irqen: bool,
121    sa1_nmien: bool,
122    sa1_irqcl: bool,
123    timer_irqcl: bool,
124    dma_irqcl: bool,
125    sa1_nmicl: bool,
126    // $220C-$220F — S-CPU NMI/IRQ vectors (when redirected).
127    snv: u16,
128    siv: u16,
129    // $2210 TMC / $2212-$2215 — H/V timer.
130    hvselb: bool,
131    ven: bool,
132    hen: bool,
133    hcnt: u16,
134    vcnt: u16,
135    // $2220-$2223 — Super-MMC bank registers.
136    cbmode: bool,
137    dbmode: bool,
138    ebmode: bool,
139    fbmode: bool,
140    cb: u32,
141    db: u32,
142    eb: u32,
143    fb: u32,
144    // $2224 BMAPS / $2225 BMAP — BW-RAM block select (S-CPU / SA-1).
145    sbm: u8,
146    sw46: bool,
147    cbm: u8,
148    // $2226 SWBE / $2227 CWBE / $2228 BWPA — BW-RAM write enable + protect.
149    swen: bool,
150    cwen: bool,
151    bwp: u8,
152    // $2229 SIWP / $222A CIWP — I-RAM write protection.
153    siwp: u8,
154    ciwp: u8,
155    // $2230 DCNT / $2231 CDMA — DMA control.
156    dmaen: bool,
157    dprio: bool,
158    cden: bool,
159    cdsel: bool,
160    dd: u8,
161    sd: u8,
162    chdend: bool,
163    dmasize: u8,
164    dmacb: u8,
165    // $2232-$2239 — DMA addresses + counter.
166    dsa: u32,
167    dda: u32,
168    dtc: u16,
169    // $223F BBF / $2240-$224F BRF — bitmap format + register file.
170    bbf: bool,
171    brf: [u8; 16],
172    // $2250-$2254 — arithmetic unit.
173    acm: bool,
174    md: bool,
175    ma: u16,
176    mb: u16,
177    // $2258-$225B — variable-length bit processing.
178    hl: bool,
179    vb: u8,
180    va: u32,
181    vbit: u8,
182    // $2300 SFR / $2301 CFR — flag reads.
183    cpu_irqfl: bool,
184    chdma_irqfl: bool,
185    sa1_irqfl: bool,
186    timer_irqfl: bool,
187    dma_irqfl: bool,
188    sa1_nmifl: bool,
189    // $2302-$2305 — latched H/V counters.
190    hcr: u16,
191    vcr: u16,
192    // $2306-$230B — arithmetic result + overflow.
193    mr: u64,
194    overflow: bool,
195}
196
197impl Io {
198    fn power() -> Self {
199        Self {
200            sa1_irq: false,
201            sa1_rdyb: false,
202            sa1_resb: true,
203            sa1_nmi: false,
204            smeg: 0,
205            cpu_irqen: false,
206            chdma_irqen: false,
207            cpu_irqcl: false,
208            chdma_irqcl: false,
209            crv: 0,
210            cnv: 0,
211            civ: 0,
212            cpu_irq: false,
213            cpu_ivsw: false,
214            cpu_nvsw: false,
215            cmeg: 0,
216            sa1_irqen: false,
217            timer_irqen: false,
218            dma_irqen: false,
219            sa1_nmien: false,
220            sa1_irqcl: false,
221            timer_irqcl: false,
222            dma_irqcl: false,
223            sa1_nmicl: false,
224            snv: 0,
225            siv: 0,
226            hvselb: false,
227            ven: false,
228            hen: false,
229            hcnt: 0,
230            vcnt: 0,
231            cbmode: false,
232            dbmode: false,
233            ebmode: false,
234            fbmode: false,
235            cb: 0,
236            db: 1,
237            eb: 2,
238            fb: 3,
239            sbm: 0,
240            sw46: false,
241            cbm: 0,
242            swen: false,
243            cwen: false,
244            bwp: 0x0f,
245            siwp: 0,
246            ciwp: 0,
247            dmaen: false,
248            dprio: false,
249            cden: false,
250            cdsel: false,
251            dd: 0,
252            sd: 0,
253            chdend: false,
254            dmasize: 0,
255            dmacb: 0,
256            dsa: 0,
257            dda: 0,
258            dtc: 0,
259            bbf: false,
260            brf: [0; 16],
261            acm: false,
262            md: false,
263            ma: 0,
264            mb: 0,
265            hl: false,
266            vb: 16,
267            va: 0,
268            vbit: 0,
269            cpu_irqfl: false,
270            chdma_irqfl: false,
271            sa1_irqfl: false,
272            timer_irqfl: false,
273            dma_irqfl: false,
274            sa1_nmifl: false,
275            hcr: 0,
276            vcr: 0,
277            mr: 0,
278            overflow: false,
279        }
280    }
281
282    /// Write every register field, in declaration order, into the caller's section.
283    fn save_state(&self, s: &mut SaveWriter) {
284        s.write_bool(self.sa1_irq);
285        s.write_bool(self.sa1_rdyb);
286        s.write_bool(self.sa1_resb);
287        s.write_bool(self.sa1_nmi);
288        s.write_u8(self.smeg);
289        s.write_bool(self.cpu_irqen);
290        s.write_bool(self.chdma_irqen);
291        s.write_bool(self.cpu_irqcl);
292        s.write_bool(self.chdma_irqcl);
293        s.write_u16(self.crv);
294        s.write_u16(self.cnv);
295        s.write_u16(self.civ);
296        s.write_bool(self.cpu_irq);
297        s.write_bool(self.cpu_ivsw);
298        s.write_bool(self.cpu_nvsw);
299        s.write_u8(self.cmeg);
300        s.write_bool(self.sa1_irqen);
301        s.write_bool(self.timer_irqen);
302        s.write_bool(self.dma_irqen);
303        s.write_bool(self.sa1_nmien);
304        s.write_bool(self.sa1_irqcl);
305        s.write_bool(self.timer_irqcl);
306        s.write_bool(self.dma_irqcl);
307        s.write_bool(self.sa1_nmicl);
308        s.write_u16(self.snv);
309        s.write_u16(self.siv);
310        s.write_bool(self.hvselb);
311        s.write_bool(self.ven);
312        s.write_bool(self.hen);
313        s.write_u16(self.hcnt);
314        s.write_u16(self.vcnt);
315        s.write_bool(self.cbmode);
316        s.write_bool(self.dbmode);
317        s.write_bool(self.ebmode);
318        s.write_bool(self.fbmode);
319        s.write_u32(self.cb);
320        s.write_u32(self.db);
321        s.write_u32(self.eb);
322        s.write_u32(self.fb);
323        s.write_u8(self.sbm);
324        s.write_bool(self.sw46);
325        s.write_u8(self.cbm);
326        s.write_bool(self.swen);
327        s.write_bool(self.cwen);
328        s.write_u8(self.bwp);
329        s.write_u8(self.siwp);
330        s.write_u8(self.ciwp);
331        s.write_bool(self.dmaen);
332        s.write_bool(self.dprio);
333        s.write_bool(self.cden);
334        s.write_bool(self.cdsel);
335        s.write_u8(self.dd);
336        s.write_u8(self.sd);
337        s.write_bool(self.chdend);
338        s.write_u8(self.dmasize);
339        s.write_u8(self.dmacb);
340        s.write_u32(self.dsa);
341        s.write_u32(self.dda);
342        s.write_u16(self.dtc);
343        s.write_bool(self.bbf);
344        s.write_bytes(&self.brf);
345        s.write_bool(self.acm);
346        s.write_bool(self.md);
347        s.write_u16(self.ma);
348        s.write_u16(self.mb);
349        s.write_bool(self.hl);
350        s.write_u8(self.vb);
351        s.write_u32(self.va);
352        s.write_u8(self.vbit);
353        s.write_bool(self.cpu_irqfl);
354        s.write_bool(self.chdma_irqfl);
355        s.write_bool(self.sa1_irqfl);
356        s.write_bool(self.timer_irqfl);
357        s.write_bool(self.dma_irqfl);
358        s.write_bool(self.sa1_nmifl);
359        s.write_u16(self.hcr);
360        s.write_u16(self.vcr);
361        s.write_u64(self.mr);
362        s.write_bool(self.overflow);
363    }
364
365    /// The inverse of [`Self::save_state`].
366    ///
367    /// # Errors
368    /// [`SaveStateError`] on truncated/corrupt input. Every field below that's masked/clamped on
369    /// every normal register write (cited per-field against the exact write site) is masked or
370    /// clamped identically on load, since several (`bwp`/`dmacb`/`dmasize` in particular) feed
371    /// bit-shift amounts elsewhere in this board — an out-of-range value from a hand-edited or
372    /// corrupted save-state would otherwise risk a shift-overflow panic.
373    fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
374        self.sa1_irq = s.read_bool()?;
375        self.sa1_rdyb = s.read_bool()?;
376        self.sa1_resb = s.read_bool()?;
377        self.sa1_nmi = s.read_bool()?;
378        self.smeg = s.read_u8()? & 0x0F; // write_io_cpu: data & 0x0f
379        self.cpu_irqen = s.read_bool()?;
380        self.chdma_irqen = s.read_bool()?;
381        self.cpu_irqcl = s.read_bool()?;
382        self.chdma_irqcl = s.read_bool()?;
383        self.crv = s.read_u16()?;
384        self.cnv = s.read_u16()?;
385        self.civ = s.read_u16()?;
386        self.cpu_irq = s.read_bool()?;
387        self.cpu_ivsw = s.read_bool()?;
388        self.cpu_nvsw = s.read_bool()?;
389        self.cmeg = s.read_u8()? & 0x0F; // write_io_sa1: data & 0x0f
390        self.sa1_irqen = s.read_bool()?;
391        self.timer_irqen = s.read_bool()?;
392        self.dma_irqen = s.read_bool()?;
393        self.sa1_nmien = s.read_bool()?;
394        self.sa1_irqcl = s.read_bool()?;
395        self.timer_irqcl = s.read_bool()?;
396        self.dma_irqcl = s.read_bool()?;
397        self.sa1_nmicl = s.read_bool()?;
398        self.snv = s.read_u16()?;
399        self.siv = s.read_u16()?;
400        self.hvselb = s.read_bool()?;
401        self.ven = s.read_bool()?;
402        self.hen = s.read_bool()?;
403        self.hcnt = s.read_u16()?;
404        self.vcnt = s.read_u16()?;
405        self.cbmode = s.read_bool()?;
406        self.dbmode = s.read_bool()?;
407        self.ebmode = s.read_bool()?;
408        self.fbmode = s.read_bool()?;
409        self.cb = s.read_u32()? & 0x07;
410        self.db = s.read_u32()? & 0x07;
411        self.eb = s.read_u32()? & 0x07;
412        self.fb = s.read_u32()? & 0x07;
413        self.sbm = s.read_u8()? & 0x1F;
414        self.sw46 = s.read_bool()?;
415        self.cbm = s.read_u8()? & 0x7F;
416        self.swen = s.read_bool()?;
417        self.cwen = s.read_bool()?;
418        self.bwp = s.read_u8()? & 0x0F;
419        self.siwp = s.read_u8()?;
420        self.ciwp = s.read_u8()?;
421        self.dmaen = s.read_bool()?;
422        self.dprio = s.read_bool()?;
423        self.cden = s.read_bool()?;
424        self.cdsel = s.read_bool()?;
425        self.dd = s.read_u8()? & 0x01;
426        self.sd = s.read_u8()? & 0x03;
427        self.chdend = s.read_bool()?;
428        // dmasize/dmacb are masked then additionally clamped on every normal write (see
429        // write_io_sa1's own `if > N { = N }` follow-up); 6-dmacb / 7-dmacb / 2-dmacb are used as
430        // subtraction-then-shift amounts elsewhere in this board, so an unclamped restored value
431        // risks the same subtraction-underflow-then-shift-overflow panic these clamps prevent.
432        self.dmasize = (s.read_u8()? & 0x07).min(5);
433        self.dmacb = (s.read_u8()? & 0x03).min(2);
434        self.dsa = s.read_u32()? & 0xFF_FFFF;
435        self.dda = s.read_u32()? & 0xFF_FFFF;
436        self.dtc = s.read_u16()?;
437        self.bbf = s.read_bool()?;
438        self.brf.copy_from_slice(s.read_bytes(16)?);
439        self.acm = s.read_bool()?;
440        self.md = s.read_bool()?;
441        self.ma = s.read_u16()?;
442        self.mb = s.read_u16()?;
443        self.hl = s.read_bool()?;
444        // vb: write_io_sa1 masks with & 0x0f then maps a masked-to-zero result to 16 (never 0).
445        let vb = s.read_u8()? & 0x0F;
446        self.vb = if vb == 0 { 16 } else { vb };
447        self.va = s.read_u32()? & 0xFF_FFFF;
448        self.vbit = s.read_u8()? & 0x07;
449        self.cpu_irqfl = s.read_bool()?;
450        self.chdma_irqfl = s.read_bool()?;
451        self.sa1_irqfl = s.read_bool()?;
452        self.timer_irqfl = s.read_bool()?;
453        self.dma_irqfl = s.read_bool()?;
454        self.sa1_nmifl = s.read_bool()?;
455        self.hcr = s.read_u16()?;
456        self.vcr = s.read_u16()?;
457        self.mr = s.read_u64()?;
458        self.overflow = s.read_bool()?;
459        Ok(())
460    }
461}
462
463/// The SA-1 H/V timer counters (ares `Status`).
464#[derive(Debug, Clone)]
465struct Timer {
466    scanlines: u16,
467    vcounter: u16,
468    hcounter: u16,
469}
470
471/// A cartridge carrying the SA-1 system: the S-CPU memory map plus the entire SA-1 ASIC + the
472/// SA-1 CPU's memory view (the second 65C816 itself lives in `rustysnes-core`).
473pub struct Sa1Board {
474    rom: Box<[u8]>,
475    bwram: Box<[u8]>,
476    iram: Box<[u8; IRAM_SIZE]>,
477    io: Io,
478    timer: Timer,
479    /// Set when the S-CPU clears RESB (1→0); consumed once by core to reset the SA-1 CPU.
480    reset_pending: bool,
481    /// `bwram.dma` — character-conversion type-1 DMA is staged; BW-RAM reads convert on the fly.
482    bwram_dma: bool,
483    /// `dma.line` — the type-2 character-conversion DMA line counter.
484    dma_line: u8,
485    /// Liveness / debugger counter: host (S-CPU) accesses to the SA-1 register window.
486    host_accesses: u64,
487}
488
489impl core::fmt::Debug for Sa1Board {
490    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
491        f.debug_struct("Sa1Board")
492            .field("rom_len", &self.rom.len())
493            .field("bwram_len", &self.bwram.len())
494            .field("sa1_resb", &self.io.sa1_resb)
495            .field("host_accesses", &self.host_accesses)
496            .finish_non_exhaustive()
497    }
498}
499
500impl Sa1Board {
501    /// Build an SA-1 board over `rom`, sizing BW-RAM from the header (clamped up to the 256 KiB
502    /// SA-1 maximum and rounded to a power of two so the linear/bitmap mirror views never wrap
503    /// short). `region` sets the timer's scanline count (262 NTSC / 312 PAL).
504    #[must_use]
505    pub fn new(rom: Box<[u8]>, sram_size: usize, region: Region) -> Self {
506        let bw_size = sram_size.max(BWRAM_MAX).next_power_of_two();
507        let scanlines = match region {
508            Region::Pal => 312,
509            Region::Ntsc => 262,
510        };
511        Self {
512            rom,
513            bwram: vec![0u8; bw_size].into_boxed_slice(),
514            iram: Box::new([0u8; IRAM_SIZE]),
515            io: Io::power(),
516            timer: Timer {
517                scanlines,
518                vcounter: 0,
519                hcounter: 0,
520            },
521            reset_pending: false,
522            bwram_dma: false,
523            dma_line: 0,
524            host_accesses: 0,
525        }
526    }
527
528    // --- ROM (Super-MMC). ------------------------------------------------------------------
529
530    fn rom_byte(&self, off: u32) -> u8 {
531        let size = self.rom.len() as u32;
532        self.rom
533            .get(mirror(off, size) as usize)
534            .copied()
535            .unwrap_or(0)
536    }
537
538    /// Read a Super-MMC-packed ROM address (ares `ROM::readCPU`). `packed` carries the lo/hi flag
539    /// in bit 22 and the 4 MiB linear offset in bits 0–21. Includes the S-CPU NMI/IRQ
540    /// vector-redirect override (`$00:FFEA/EE` → SNV/SIV when armed).
541    fn rom_read_packed(&self, packed: u32) -> u8 {
542        // Reset/NMI/IRQ vector overrides ($00:FFE0-FFEF, packed as $007FE0-$007FEF).
543        if packed & 0xff_ffe0 == 0x00_7fe0 {
544            match packed {
545                0x7fea if self.io.cpu_nvsw => return self.io.snv as u8,
546                0x7feb if self.io.cpu_nvsw => return (self.io.snv >> 8) as u8,
547                0x7fee if self.io.cpu_ivsw => return self.io.siv as u8,
548                0x7fef if self.io.cpu_ivsw => return (self.io.siv >> 8) as u8,
549                _ => {}
550            }
551        }
552        let lo = packed < 0x40_0000;
553        let address = packed & 0x3f_ffff;
554        let block = address >> 20; // 0..3 selects C/D/E/F
555        let within = address & 0x0f_ffff;
556        let (mode, bank) = match block {
557            0 => (self.io.cbmode, self.io.cb),
558            1 => (self.io.dbmode, self.io.db),
559            2 => (self.io.ebmode, self.io.eb),
560            _ => (self.io.fbmode, self.io.fb),
561        };
562        if lo && !mode {
563            self.rom_byte(address)
564        } else {
565            self.rom_byte((bank << 20) | within)
566        }
567    }
568
569    /// Pack an S-CPU ROM address `(bank, addr)` into the MMC linear space.
570    fn rom_cpu_packed(bank: u32, addr: u32) -> u32 {
571        if bank >= 0xC0 {
572            // HiROM-style: $C0-$FF:$0000-$FFFF (bit 22 set).
573            let block = (bank - 0xC0) >> 4; // C0-CF=0, D0-DF=1, E0-EF=2, F0-FF=3
574            let within = ((bank & 0x0F) << 16) | addr;
575            0x40_0000 | (block << 20) | within
576        } else {
577            // LoROM-style window: $00-$1F=C, $20-$3F=D, $80-$9F=E, $A0-$BF=F.
578            let block = match bank {
579                0x00..=0x1F => 0,
580                0x20..=0x3F => 1,
581                0x80..=0x9F => 2,
582                _ => 3,
583            };
584            let within = ((bank & 0x1F) << 15) | (addr & 0x7FFF);
585            (block << 20) | within
586        }
587    }
588
589    fn rom_read_cpu(&self, bank: u32, addr: u32) -> u8 {
590        self.rom_read_packed(Self::rom_cpu_packed(bank, addr))
591    }
592
593    /// SA-1-view ROM read (ares `ROM::readSA1`): translate the SA-1 `$8000-$FFFF` window, then run
594    /// the shared MMC decode.
595    fn rom_read_sa1(&self, address: u32) -> u8 {
596        let packed = if address & 0x40_8000 == 0x00_8000 {
597            ((address & 0x80_0000) >> 2) | ((address & 0x3f_0000) >> 1) | (address & 0x00_7fff)
598        } else {
599            address
600        };
601        self.rom_read_packed(packed)
602    }
603
604    // --- BW-RAM. ---------------------------------------------------------------------------
605
606    fn bwram_read(&self, off: u32) -> u8 {
607        if self.bwram.is_empty() {
608            return 0;
609        }
610        let size = self.bwram.len() as u32;
611        self.bwram
612            .get(mirror(off, size) as usize)
613            .copied()
614            .unwrap_or(0)
615    }
616
617    fn bwram_write(&mut self, off: u32, val: u8) {
618        if self.bwram.is_empty() {
619            return;
620        }
621        let size = self.bwram.len() as u32;
622        let idx = mirror(off, size) as usize;
623        if let Some(slot) = self.bwram.get_mut(idx) {
624            *slot = val;
625        }
626    }
627
628    /// BW-RAM bitmap read (ares `BWRAM::readBitmap`): 2/4 bpp virtual unpack of `$60-$6F`.
629    fn bwram_read_bitmap(&self, address: u32) -> u8 {
630        if !self.io.bbf {
631            // 4 bpp
632            let shift = address & 1;
633            let byte = self.bwram_read(address >> 1);
634            if shift == 0 { byte & 0x0F } else { byte >> 4 }
635        } else {
636            // 2 bpp
637            let shift = address & 3;
638            let byte = self.bwram_read(address >> 2);
639            (byte >> (shift * 2)) & 0x03
640        }
641    }
642
643    /// BW-RAM bitmap write (ares `BWRAM::writeBitmap`).
644    fn bwram_write_bitmap(&mut self, address: u32, val: u8) {
645        if !self.io.bbf {
646            let shift = address & 1;
647            let a = address >> 1;
648            let cur = self.bwram_read(a);
649            let data = if shift == 0 {
650                (cur & 0xF0) | (val & 0x0F)
651            } else {
652                (cur & 0x0F) | ((val & 0x0F) << 4)
653            };
654            self.bwram_write(a, data);
655        } else {
656            let shift = address & 3;
657            let a = address >> 2;
658            let cur = self.bwram_read(a);
659            let mask = !(0x03u8 << (shift * 2));
660            let data = (cur & mask) | ((val & 0x03) << (shift * 2));
661            self.bwram_write(a, data);
662        }
663    }
664
665    /// BW-RAM linear write (ares `BWRAM::writeLinear`) with the SWEN/CWEN/BWPA write-protect.
666    fn bwram_write_linear(&mut self, off: u32, val: u8) {
667        if !self.io.swen && !self.io.cwen && (off & 0x3_ffff) < (0x100u32 << self.io.bwp) {
668            return;
669        }
670        self.bwram_write(off, val);
671    }
672
673    fn bwram_read_cpu(&mut self, bank: u32, addr: u32) -> u8 {
674        if (0x6000..=0x7FFF).contains(&addr) {
675            let off = self.io.sbm as u32 * 0x2000 + (addr & 0x1FFF);
676            if self.bwram_dma {
677                return self.dma_cc1_read(off);
678            }
679            return self.bwram_read(off);
680        }
681        // $40-$4F linear.
682        let raw = (bank << 16) | addr;
683        self.bwram_read(raw)
684    }
685
686    fn bwram_write_cpu(&mut self, bank: u32, addr: u32, val: u8) {
687        let off = if (0x6000..=0x7FFF).contains(&addr) {
688            self.io.sbm as u32 * 0x2000 + (addr & 0x1FFF)
689        } else {
690            (bank << 16) | addr
691        };
692        if !self.io.swen && !self.io.cwen && (off & 0x3_ffff) < (0x100u32 << self.io.bwp) {
693            return;
694        }
695        self.bwram_write(off, val);
696    }
697
698    /// SA-1-view BW-RAM read (ares `memory.cpp` read() BW-RAM branch + `BWRAM::readSA1`).
699    fn bwram_read_sa1(&self, address: u32) -> u8 {
700        if address & 0x40_0000 != 0 && address & 0x20_0000 != 0 {
701            // $60-$6F bitmap window.
702            return self.bwram_read_bitmap(address & 0x0f_ffff);
703        }
704        if address & 0x40_0000 != 0 {
705            // $40-$5F linear.
706            return self.bwram_read(address);
707        }
708        // $6000-$7FFF window.
709        if !self.io.sw46 {
710            self.bwram_read((self.io.cbm as u32 & 0x1f) * 0x2000 + (address & 0x1fff))
711        } else {
712            self.bwram_read_bitmap(self.io.cbm as u32 * 0x2000 + (address & 0x1fff))
713        }
714    }
715
716    fn bwram_write_sa1(&mut self, address: u32, val: u8) {
717        if address & 0x40_0000 != 0 && address & 0x20_0000 != 0 {
718            self.bwram_write_bitmap(address & 0x0f_ffff, val);
719            return;
720        }
721        if address & 0x40_0000 != 0 {
722            self.bwram_write_linear(address, val);
723            return;
724        }
725        if !self.io.sw46 {
726            self.bwram_write_linear(
727                (self.io.cbm as u32 & 0x1f) * 0x2000 + (address & 0x1fff),
728                val,
729            );
730        } else {
731            self.bwram_write_bitmap(self.io.cbm as u32 * 0x2000 + (address & 0x1fff), val);
732        }
733    }
734
735    // --- I-RAM. ----------------------------------------------------------------------------
736
737    fn iram_read(&self, off: u32) -> u8 {
738        self.iram[(off & 0x7FF) as usize]
739    }
740
741    fn iram_write(&mut self, off: u32, val: u8) {
742        self.iram[(off & 0x7FF) as usize] = val;
743    }
744
745    fn iram_write_cpu(&mut self, off: u32, val: u8) {
746        let block = (off >> 8) & 0x7;
747        if self.io.siwp & (1 << block) == 0 {
748            return;
749        }
750        self.iram_write(off, val);
751    }
752
753    fn iram_write_sa1(&mut self, off: u32, val: u8) {
754        let block = (off >> 8) & 0x7;
755        if self.io.ciwp & (1 << block) == 0 {
756            return;
757        }
758        self.iram_write(off, val);
759    }
760
761    // --- Variable-length bit read (ares `readVBR`). ----------------------------------------
762
763    fn read_vbr(&self, address: u32) -> u8 {
764        if address & 0x40_8000 == 0x00_8000 || address & 0xc0_0000 == 0xc0_0000 {
765            return self.rom_read_sa1(address);
766        }
767        if address & 0x40_e000 == 0x00_6000 || address & 0xf0_0000 == 0x40_0000 {
768            return self.bwram_read(address);
769        }
770        if address & 0x40_f800 == 0x00_0000 || address & 0x40_f800 == 0x00_3000 {
771            return self.iram_read(address);
772        }
773        0xff
774    }
775
776    // --- Arithmetic unit ($2254 trigger). --------------------------------------------------
777
778    fn arith_trigger(&mut self) {
779        if !self.io.acm {
780            if !self.io.md {
781                // signed multiplication
782                self.io.mr =
783                    ((self.io.ma as i16 as i32) * (self.io.mb as i16 as i32)) as u32 as u64;
784                self.io.mb = 0;
785            } else {
786                // unsigned division
787                if self.io.mb == 0 {
788                    self.io.mr = 0;
789                } else {
790                    let dividend = self.io.ma as i16 as i32;
791                    let divisor = self.io.mb as i32;
792                    let remainder = if dividend >= 0 {
793                        (dividend % divisor) as u16
794                    } else {
795                        (((dividend % divisor) + divisor) % divisor) as u16
796                    };
797                    let quotient = ((dividend - remainder as i32) / divisor) as u16;
798                    self.io.mr = ((remainder as u64) << 16) | quotient as u64;
799                }
800                self.io.ma = 0;
801                self.io.mb = 0;
802            }
803        } else {
804            // cumulative sum (sigma)
805            let prod = (self.io.ma as i16 as i64) * (self.io.mb as i16 as i64);
806            self.io.mr = (self.io.mr as i64).wrapping_add(prod) as u64;
807            self.io.overflow = (self.io.mr >> 40) & 1 != 0;
808            self.io.mr &= 0xff_ffff_ffff;
809            self.io.mb = 0;
810        }
811    }
812
813    // --- Timer (ares `SA1::step`). ---------------------------------------------------------
814
815    fn timer_trigger_irq(&mut self) {
816        self.io.timer_irqfl = true;
817        if self.io.timer_irqen {
818            self.io.timer_irqcl = false;
819        }
820    }
821
822    /// Advance the H/V timer by `clocks` SA-1 master clocks (always even; 2 clocks per CPU cycle),
823    /// firing the timer IRQ on an exact comparator match.
824    fn tick(&mut self, clocks: u32) {
825        let mut remaining = clocks;
826        while remaining >= 2 {
827            remaining -= 2;
828            if !self.io.hvselb {
829                self.timer.hcounter = self.timer.hcounter.wrapping_add(2);
830                if self.timer.hcounter >= 1364 {
831                    self.timer.hcounter = 0;
832                    self.timer.vcounter = self.timer.vcounter.wrapping_add(1);
833                    if self.timer.vcounter >= self.timer.scanlines {
834                        self.timer.vcounter = 0;
835                    }
836                }
837            } else {
838                self.timer.hcounter = self.timer.hcounter.wrapping_add(2);
839                self.timer.vcounter = self.timer.vcounter.wrapping_add(self.timer.hcounter >> 11);
840                self.timer.hcounter &= 0x07ff;
841                self.timer.vcounter &= 0x01ff;
842            }
843            match (self.io.hen, self.io.ven) {
844                (false, false) => {}
845                (true, false) => {
846                    if self.timer.hcounter == self.io.hcnt << 2 {
847                        self.timer_trigger_irq();
848                    }
849                }
850                (false, true) => {
851                    if self.timer.vcounter == self.io.vcnt && self.timer.hcounter == 0 {
852                        self.timer_trigger_irq();
853                    }
854                }
855                (true, true) => {
856                    if self.timer.vcounter == self.io.vcnt
857                        && self.timer.hcounter == self.io.hcnt << 2
858                    {
859                        self.timer_trigger_irq();
860                    }
861                }
862            }
863        }
864    }
865
866    // --- DMA (ares `dma.cpp`). -------------------------------------------------------------
867
868    /// Normal (direct) DMA: ROM/BW-RAM/I-RAM → BW-RAM/I-RAM, `dtc` bytes.
869    fn dma_normal(&mut self) {
870        const SOURCE_ROM: u8 = 0;
871        const SOURCE_BWRAM: u8 = 1;
872        const SOURCE_IRAM: u8 = 2;
873        const DEST_IRAM: u8 = 0;
874        const DEST_BWRAM: u8 = 1;
875        while self.io.dtc != 0 {
876            self.io.dtc = self.io.dtc.wrapping_sub(1);
877            let source = self.io.dsa;
878            let target = self.io.dda;
879            self.io.dsa = self.io.dsa.wrapping_add(1) & 0xff_ffff;
880            self.io.dda = self.io.dda.wrapping_add(1) & 0xff_ffff;
881            match (self.io.sd, self.io.dd) {
882                (SOURCE_ROM, DEST_BWRAM) => {
883                    let data = self.rom_read_sa1(source);
884                    self.bwram_write(target, data);
885                }
886                (SOURCE_ROM, DEST_IRAM) => {
887                    let data = self.rom_read_sa1(source);
888                    self.iram_write(target, data);
889                }
890                (SOURCE_BWRAM, DEST_IRAM) => {
891                    let data = self.bwram_read(source);
892                    self.iram_write(target, data);
893                }
894                (SOURCE_IRAM, DEST_BWRAM) => {
895                    let data = self.iram_read(source);
896                    self.bwram_write(target, data);
897                }
898                _ => {}
899            }
900        }
901        self.io.dma_irqfl = true;
902        if self.io.dma_irqen {
903            self.io.dma_irqcl = false;
904        }
905    }
906
907    /// Type-1 character-conversion DMA arm (ares `dmaCC1`): stages BW-RAM-side conversion and
908    /// raises the character-conversion IRQ to the S-CPU.
909    fn dma_cc1(&mut self) {
910        self.bwram_dma = true;
911        self.io.chdma_irqfl = true;
912        if self.io.chdma_irqen {
913            self.io.chdma_irqcl = false;
914        }
915    }
916
917    /// Type-1 character-conversion BW-RAM read (ares `dmaCC1Read`): on a character boundary,
918    /// transcode the next tile from BW-RAM (linear) into I-RAM (planar), then serve from I-RAM.
919    fn dma_cc1_read(&mut self, address: u32) -> u8 {
920        let charmask = (1u32 << (6 - self.io.dmacb)) - 1;
921        if address & charmask == 0 {
922            let bpp = 2u32 << (2 - self.io.dmacb);
923            let bpl = (8u32 << self.io.dmasize) >> self.io.dmacb;
924            let bwmask = self.bwram.len() as u32 - 1;
925            let tile = ((address.wrapping_sub(self.io.dsa)) & bwmask) >> (6 - self.io.dmacb);
926            let ty = tile >> self.io.dmasize;
927            let tx = tile & ((1 << self.io.dmasize) - 1);
928            let mut bwaddr = self.io.dsa + ty * 8 * bpl + tx * bpp;
929            for y in 0..8u32 {
930                let mut data: u64 = 0;
931                for byte in 0..bpp {
932                    data |= (self.bwram_read((bwaddr + byte) & bwmask) as u64) << (byte << 3);
933                }
934                bwaddr += bpl;
935                let mut out = [0u8; 8];
936                for x in 0..8u32 {
937                    out[0] |= ((data & 1) as u8) << (7 - x);
938                    data >>= 1;
939                    out[1] |= ((data & 1) as u8) << (7 - x);
940                    data >>= 1;
941                    if self.io.dmacb == 2 {
942                        continue;
943                    }
944                    out[2] |= ((data & 1) as u8) << (7 - x);
945                    data >>= 1;
946                    out[3] |= ((data & 1) as u8) << (7 - x);
947                    data >>= 1;
948                    if self.io.dmacb == 1 {
949                        continue;
950                    }
951                    out[4] |= ((data & 1) as u8) << (7 - x);
952                    data >>= 1;
953                    out[5] |= ((data & 1) as u8) << (7 - x);
954                    data >>= 1;
955                    out[6] |= ((data & 1) as u8) << (7 - x);
956                    data >>= 1;
957                    out[7] |= ((data & 1) as u8) << (7 - x);
958                    data >>= 1;
959                }
960                for byte in 0..bpp {
961                    let p = self.io.dda + (y << 1) + ((byte & 6) << 3) + (byte & 1);
962                    self.iram_write(p & 0x07ff, out[byte as usize]);
963                }
964            }
965        }
966        self.iram_read((self.io.dda + (address & charmask)) & 0x07ff)
967    }
968
969    /// Type-2 character-conversion DMA (ares `dmaCC2`): pack the bitmap register file into I-RAM
970    /// planar form, advancing the line counter.
971    fn dma_cc2(&mut self) {
972        let base = ((self.dma_line & 1) << 3) as usize;
973        let bpp = 2u32 << (2 - self.io.dmacb);
974        let mut address = self.io.dda & 0x07ff;
975        address &= !((1u32 << (7 - self.io.dmacb)) - 1);
976        address += (self.dma_line as u32 & 8) * bpp;
977        address += (self.dma_line as u32 & 7) * 2;
978        for byte in 0..bpp {
979            let mut output = 0u8;
980            for bit in 0..8u32 {
981                output |= ((self.io.brf[base + bit as usize] >> byte) & 1) << (7 - bit);
982            }
983            self.iram_write(address + ((byte & 6) << 3) + (byte & 1), output);
984        }
985        self.dma_line = (self.dma_line + 1) & 15;
986    }
987
988    // --- I/O register access (ares `io.cpp`). ----------------------------------------------
989
990    fn read_io_cpu(&self, address: u32) -> u8 {
991        match 0x2200 | (address & 0x1FF) {
992            // (SFR) S-CPU flag read.
993            0x2300 => {
994                (self.io.cmeg & 0x0f)
995                    | (u8::from(self.io.cpu_nvsw) << 4)
996                    | (u8::from(self.io.chdma_irqfl) << 5)
997                    | (u8::from(self.io.cpu_ivsw) << 6)
998                    | (u8::from(self.io.cpu_irqfl) << 7)
999            }
1000            _ => 0,
1001        }
1002    }
1003
1004    fn read_io_sa1(&mut self, address: u32) -> u8 {
1005        match 0x2200 | (address & 0x1FF) {
1006            // (CFR) SA-1 flag read.
1007            0x2301 => {
1008                (self.io.smeg & 0x0f)
1009                    | (u8::from(self.io.sa1_nmifl) << 4)
1010                    | (u8::from(self.io.dma_irqfl) << 5)
1011                    | (u8::from(self.io.timer_irqfl) << 6)
1012                    | (u8::from(self.io.sa1_irqfl) << 7)
1013            }
1014            // (HCR/VCR) latch counters then read low byte.
1015            0x2302 => {
1016                self.io.hcr = self.timer.hcounter >> 2;
1017                self.io.vcr = self.timer.vcounter;
1018                self.io.hcr as u8
1019            }
1020            0x2303 => (self.io.hcr >> 8) as u8,
1021            0x2304 => self.io.vcr as u8,
1022            0x2305 => (self.io.vcr >> 8) as u8,
1023            // (MR) arithmetic result, 5 bytes.
1024            0x2306 => self.io.mr as u8,
1025            0x2307 => (self.io.mr >> 8) as u8,
1026            0x2308 => (self.io.mr >> 16) as u8,
1027            0x2309 => (self.io.mr >> 24) as u8,
1028            0x230a => (self.io.mr >> 32) as u8,
1029            // (OF) overflow.
1030            0x230b => u8::from(self.io.overflow) << 7,
1031            // (VDPL/VDPH) variable-length data read.
1032            0x230c => {
1033                let data = self.varlen_word();
1034                data as u8
1035            }
1036            0x230d => {
1037                let data = self.varlen_word();
1038                if self.io.hl {
1039                    self.io.vbit += self.io.vb;
1040                    self.io.va = self.io.va.wrapping_add((self.io.vbit >> 3) as u32) & 0xff_ffff;
1041                    self.io.vbit &= 7;
1042                }
1043                (data >> 8) as u8
1044            }
1045            _ => 0xff,
1046        }
1047    }
1048
1049    fn varlen_word(&self) -> u32 {
1050        let mut data = self.read_vbr(self.io.va) as u32;
1051        data |= (self.read_vbr(self.io.va.wrapping_add(1) & 0xff_ffff) as u32) << 8;
1052        data |= (self.read_vbr(self.io.va.wrapping_add(2) & 0xff_ffff) as u32) << 16;
1053        data >> self.io.vbit
1054    }
1055
1056    fn write_io_cpu(&mut self, address: u32, data: u8) {
1057        match 0x2200 | (address & 0x1FF) {
1058            // (CCNT) SA-1 control.
1059            0x2200 => {
1060                if self.io.sa1_resb && data & 0x20 == 0 {
1061                    // RESB 1→0: reset the SA-1 CPU.
1062                    self.reset_pending = true;
1063                    self.io.ciwp = 0x00;
1064                }
1065                self.io.smeg = data & 0x0f;
1066                self.io.sa1_nmi = data & 0x10 != 0;
1067                self.io.sa1_resb = data & 0x20 != 0;
1068                self.io.sa1_rdyb = data & 0x40 != 0;
1069                self.io.sa1_irq = data & 0x80 != 0;
1070                if self.io.sa1_irq {
1071                    self.io.sa1_irqfl = true;
1072                    if self.io.sa1_irqen {
1073                        self.io.sa1_irqcl = false;
1074                    }
1075                }
1076                if self.io.sa1_nmi {
1077                    self.io.sa1_nmifl = true;
1078                    if self.io.sa1_nmien {
1079                        self.io.sa1_nmicl = false;
1080                    }
1081                }
1082            }
1083            // (SIE) S-CPU interrupt enable.
1084            0x2201 => {
1085                self.io.chdma_irqen = data & 0x20 != 0;
1086                self.io.cpu_irqen = data & 0x80 != 0;
1087            }
1088            // (SIC) S-CPU interrupt clear.
1089            0x2202 => {
1090                self.io.chdma_irqcl = data & 0x20 != 0;
1091                self.io.cpu_irqcl = data & 0x80 != 0;
1092                if self.io.chdma_irqcl {
1093                    self.io.chdma_irqfl = false;
1094                }
1095                if self.io.cpu_irqcl {
1096                    self.io.cpu_irqfl = false;
1097                }
1098            }
1099            // (CRV) SA-1 reset vector.
1100            0x2203 => self.io.crv = (self.io.crv & 0xff00) | data as u16,
1101            0x2204 => self.io.crv = (self.io.crv & 0x00ff) | ((data as u16) << 8),
1102            // (CNV) SA-1 NMI vector.
1103            0x2205 => self.io.cnv = (self.io.cnv & 0xff00) | data as u16,
1104            0x2206 => self.io.cnv = (self.io.cnv & 0x00ff) | ((data as u16) << 8),
1105            // (CIV) SA-1 IRQ vector.
1106            0x2207 => self.io.civ = (self.io.civ & 0xff00) | data as u16,
1107            0x2208 => self.io.civ = (self.io.civ & 0x00ff) | ((data as u16) << 8),
1108            // (CXB/DXB/EXB/FXB) Super-MMC banks.
1109            0x2220 => {
1110                self.io.cb = (data & 0x07) as u32;
1111                self.io.cbmode = data & 0x80 != 0;
1112            }
1113            0x2221 => {
1114                self.io.db = (data & 0x07) as u32;
1115                self.io.dbmode = data & 0x80 != 0;
1116            }
1117            0x2222 => {
1118                self.io.eb = (data & 0x07) as u32;
1119                self.io.ebmode = data & 0x80 != 0;
1120            }
1121            0x2223 => {
1122                self.io.fb = (data & 0x07) as u32;
1123                self.io.fbmode = data & 0x80 != 0;
1124            }
1125            // (BMAPS) S-CPU BW-RAM block.
1126            0x2224 => self.io.sbm = data & 0x1f,
1127            // (SWBE) S-CPU BW-RAM write enable.
1128            0x2226 => self.io.swen = data & 0x80 != 0,
1129            // (BWPA) BW-RAM write-protect area.
1130            0x2228 => self.io.bwp = data & 0x0f,
1131            // (SIWP) S-CPU I-RAM write protect.
1132            0x2229 => self.io.siwp = data,
1133            0x2231..=0x2237 => self.write_io_shared(address, data),
1134            _ => {}
1135        }
1136    }
1137
1138    fn write_io_sa1(&mut self, address: u32, data: u8) {
1139        match 0x2200 | (address & 0x1FF) {
1140            // (SCNT) S-CPU control.
1141            0x2209 => {
1142                self.io.cmeg = data & 0x0f;
1143                self.io.cpu_nvsw = data & 0x10 != 0;
1144                self.io.cpu_ivsw = data & 0x40 != 0;
1145                self.io.cpu_irq = data & 0x80 != 0;
1146                if self.io.cpu_irq {
1147                    self.io.cpu_irqfl = true;
1148                    if self.io.cpu_irqen {
1149                        self.io.cpu_irqcl = false;
1150                    }
1151                }
1152            }
1153            // (CIE) SA-1 interrupt enable.
1154            0x220a => {
1155                self.io.sa1_nmien = data & 0x10 != 0;
1156                self.io.dma_irqen = data & 0x20 != 0;
1157                self.io.timer_irqen = data & 0x40 != 0;
1158                self.io.sa1_irqen = data & 0x80 != 0;
1159            }
1160            // (CIC) SA-1 interrupt clear.
1161            0x220b => {
1162                self.io.sa1_nmicl = data & 0x10 != 0;
1163                self.io.dma_irqcl = data & 0x20 != 0;
1164                self.io.timer_irqcl = data & 0x40 != 0;
1165                self.io.sa1_irqcl = data & 0x80 != 0;
1166                if self.io.sa1_nmicl {
1167                    self.io.sa1_nmifl = false;
1168                }
1169                if self.io.sa1_irqcl {
1170                    self.io.sa1_irqfl = false;
1171                }
1172                if self.io.timer_irqcl {
1173                    self.io.timer_irqfl = false;
1174                }
1175                if self.io.dma_irqcl {
1176                    self.io.dma_irqfl = false;
1177                }
1178            }
1179            // (SNV) S-CPU NMI vector.
1180            0x220c => self.io.snv = (self.io.snv & 0xff00) | data as u16,
1181            0x220d => self.io.snv = (self.io.snv & 0x00ff) | ((data as u16) << 8),
1182            // (SIV) S-CPU IRQ vector.
1183            0x220e => self.io.siv = (self.io.siv & 0xff00) | data as u16,
1184            0x220f => self.io.siv = (self.io.siv & 0x00ff) | ((data as u16) << 8),
1185            // (TMC) H/V timer control.
1186            0x2210 => {
1187                self.io.hen = data & 0x01 != 0;
1188                self.io.ven = data & 0x02 != 0;
1189                self.io.hvselb = data & 0x80 != 0;
1190            }
1191            // (CTR) SA-1 timer restart.
1192            0x2211 => {
1193                self.timer.vcounter = 0;
1194                self.timer.hcounter = 0;
1195            }
1196            // (HCNT/VCNT).
1197            0x2212 => self.io.hcnt = (self.io.hcnt & 0xff00) | data as u16,
1198            0x2213 => self.io.hcnt = (self.io.hcnt & 0x00ff) | ((data as u16) << 8),
1199            0x2214 => self.io.vcnt = (self.io.vcnt & 0xff00) | data as u16,
1200            0x2215 => self.io.vcnt = (self.io.vcnt & 0x00ff) | ((data as u16) << 8),
1201            // (BMAP) SA-1 BW-RAM block.
1202            0x2225 => {
1203                self.io.cbm = data & 0x7f;
1204                self.io.sw46 = data & 0x80 != 0;
1205            }
1206            // (CWBE) SA-1 BW-RAM write enable.
1207            0x2227 => self.io.cwen = data & 0x80 != 0,
1208            // (CIWP) SA-1 I-RAM write protect.
1209            0x222a => self.io.ciwp = data,
1210            // (DCNT) DMA control.
1211            0x2230 => {
1212                self.io.sd = data & 0x03;
1213                self.io.dd = (data >> 2) & 1;
1214                self.io.cdsel = data & 0x10 != 0;
1215                self.io.cden = data & 0x20 != 0;
1216                self.io.dprio = data & 0x40 != 0;
1217                self.io.dmaen = data & 0x80 != 0;
1218                if !self.io.dmaen {
1219                    self.dma_line = 0;
1220                }
1221            }
1222            0x2231..=0x2237 => self.write_io_shared(address, data),
1223            // (DTC) DMA terminal counter.
1224            0x2238 => self.io.dtc = (self.io.dtc & 0xff00) | data as u16,
1225            0x2239 => self.io.dtc = (self.io.dtc & 0x00ff) | ((data as u16) << 8),
1226            // (BBF) bitmap format.
1227            0x223f => self.io.bbf = data & 0x80 != 0,
1228            // (BRF) bitmap register files.
1229            0x2240..=0x224f => {
1230                let idx = (address & 0x0f) as usize;
1231                self.io.brf[idx] = data;
1232                if (idx == 7 || idx == 15) && self.io.dmaen && self.io.cden && !self.io.cdsel {
1233                    self.dma_cc2();
1234                }
1235            }
1236            // (MCNT) arithmetic control.
1237            0x2250 => {
1238                self.io.md = data & 0x01 != 0;
1239                self.io.acm = data & 0x02 != 0;
1240                if self.io.acm {
1241                    self.io.mr = 0;
1242                }
1243            }
1244            // (MA/MB) operands.
1245            0x2251 => self.io.ma = (self.io.ma & 0xff00) | data as u16,
1246            0x2252 => self.io.ma = (self.io.ma & 0x00ff) | ((data as u16) << 8),
1247            0x2253 => self.io.mb = (self.io.mb & 0xff00) | data as u16,
1248            0x2254 => {
1249                self.io.mb = (self.io.mb & 0x00ff) | ((data as u16) << 8);
1250                self.arith_trigger();
1251            }
1252            // (VBD) variable-length bit control.
1253            0x2258 => {
1254                self.io.vb = data & 0x0f;
1255                self.io.hl = data & 0x80 != 0;
1256                if self.io.vb == 0 {
1257                    self.io.vb = 16;
1258                }
1259                if !self.io.hl {
1260                    self.io.vbit += self.io.vb;
1261                    self.io.va = self.io.va.wrapping_add((self.io.vbit >> 3) as u32) & 0xff_ffff;
1262                    self.io.vbit &= 7;
1263                }
1264            }
1265            // (VDA) variable-length start address.
1266            0x2259 => self.io.va = (self.io.va & 0xff_ff00) | data as u32,
1267            0x225a => self.io.va = (self.io.va & 0xff_00ff) | ((data as u32) << 8),
1268            0x225b => {
1269                self.io.va = (self.io.va & 0x00_ffff) | ((data as u32) << 16);
1270                self.io.vbit = 0;
1271            }
1272            _ => {}
1273        }
1274    }
1275
1276    fn write_io_shared(&mut self, address: u32, data: u8) {
1277        match 0x2200 | (address & 0x1FF) {
1278            // (CDMA) character-conversion DMA parameters.
1279            0x2231 => {
1280                self.io.dmacb = data & 0x03;
1281                self.io.dmasize = (data >> 2) & 0x07;
1282                self.io.chdend = data & 0x80 != 0;
1283                if self.io.dmacb > 2 {
1284                    self.io.dmacb = 2;
1285                }
1286                if self.io.dmasize > 5 {
1287                    self.io.dmasize = 5;
1288                }
1289                if self.io.chdend {
1290                    self.bwram_dma = false;
1291                }
1292            }
1293            // (SDA) DMA source.
1294            0x2232 => self.io.dsa = (self.io.dsa & 0xff_ff00) | data as u32,
1295            0x2233 => self.io.dsa = (self.io.dsa & 0xff_00ff) | ((data as u32) << 8),
1296            0x2234 => self.io.dsa = (self.io.dsa & 0x00_ffff) | ((data as u32) << 16),
1297            // (DDA) DMA destination.
1298            0x2235 => self.io.dda = (self.io.dda & 0xff_ff00) | data as u32,
1299            0x2236 => {
1300                self.io.dda = (self.io.dda & 0xff_00ff) | ((data as u32) << 8);
1301                if self.io.dmaen {
1302                    if !self.io.cden && self.io.dd == 0 {
1303                        self.dma_normal();
1304                    } else if self.io.cden && self.io.cdsel {
1305                        self.dma_cc1();
1306                    }
1307                }
1308            }
1309            0x2237 => {
1310                self.io.dda = (self.io.dda & 0x00_ffff) | ((data as u32) << 16);
1311                if self.io.dmaen && !self.io.cden && self.io.dd == 1 {
1312                    self.dma_normal();
1313                }
1314            }
1315            _ => {}
1316        }
1317    }
1318
1319    // --- The S-CPU (main) memory decode ($00-$3F/$80-$BF + $40-$4F + $C0-$FF). --------------
1320
1321    fn classify_cpu(addr24: u32) -> CpuRegion {
1322        let bank = (addr24 >> 16) & 0xFF;
1323        let addr = addr24 & 0xFFFF;
1324        let lo = bank & 0x7F; // fold $80-$BF onto $00-$3F for the windowed regions
1325        if lo <= 0x3F {
1326            if (0x2200..=0x23FF).contains(&addr) {
1327                return CpuRegion::Io;
1328            }
1329            if (0x3000..=0x37FF).contains(&addr) {
1330                return CpuRegion::Iram(addr & 0x7FF);
1331            }
1332            if (0x6000..=0x7FFF).contains(&addr) {
1333                return CpuRegion::Bwram;
1334            }
1335            if addr >= 0x8000 {
1336                return CpuRegion::Rom;
1337            }
1338        }
1339        if (0x40..=0x4F).contains(&bank) {
1340            return CpuRegion::Bwram;
1341        }
1342        if bank >= 0xC0 {
1343            return CpuRegion::Rom;
1344        }
1345        CpuRegion::Open
1346    }
1347}
1348
1349/// What a 24-bit S-CPU address decodes to on an SA-1 board.
1350enum CpuRegion {
1351    Io,
1352    Rom,
1353    Bwram,
1354    Iram(u32),
1355    Open,
1356}
1357
1358impl Board for Sa1Board {
1359    fn name(&self) -> &'static str {
1360        "SA-1"
1361    }
1362
1363    fn coprocessor(&self) -> Coprocessor {
1364        Coprocessor::Sa1
1365    }
1366
1367    fn map(&self, addr24: u32) -> MappedAddr {
1368        match Self::classify_cpu(addr24) {
1369            CpuRegion::Io | CpuRegion::Iram(_) => MappedAddr::Coprocessor,
1370            CpuRegion::Rom => MappedAddr::Rom(0),
1371            CpuRegion::Bwram => MappedAddr::Sram(0),
1372            CpuRegion::Open => MappedAddr::Open,
1373        }
1374    }
1375
1376    fn read24(&mut self, addr24: u32) -> u8 {
1377        let bank = (addr24 >> 16) & 0xFF;
1378        let addr = addr24 & 0xFFFF;
1379        match Self::classify_cpu(addr24) {
1380            CpuRegion::Io => {
1381                self.host_accesses = self.host_accesses.wrapping_add(1);
1382                self.read_io_cpu(addr24)
1383            }
1384            CpuRegion::Rom => {
1385                let b = if bank >= 0xC0 { bank } else { bank & 0x7F };
1386                self.rom_read_cpu(b, addr)
1387            }
1388            CpuRegion::Bwram => {
1389                let b = if (0x40..=0x4F).contains(&bank) {
1390                    bank
1391                } else {
1392                    bank & 0x7F
1393                };
1394                self.bwram_read_cpu(b, addr)
1395            }
1396            CpuRegion::Iram(off) => self.iram_read(off),
1397            CpuRegion::Open => 0,
1398        }
1399    }
1400
1401    fn write24(&mut self, addr24: u32, val: u8) {
1402        let bank = (addr24 >> 16) & 0xFF;
1403        let addr = addr24 & 0xFFFF;
1404        match Self::classify_cpu(addr24) {
1405            CpuRegion::Io => {
1406                self.host_accesses = self.host_accesses.wrapping_add(1);
1407                self.write_io_cpu(addr24, val);
1408            }
1409            CpuRegion::Bwram => {
1410                let b = if (0x40..=0x4F).contains(&bank) {
1411                    bank
1412                } else {
1413                    bank & 0x7F
1414                };
1415                self.bwram_write_cpu(b, addr, val);
1416            }
1417            CpuRegion::Iram(off) => self.iram_write_cpu(off, val),
1418            CpuRegion::Rom | CpuRegion::Open => {}
1419        }
1420    }
1421
1422    fn rom(&self) -> &[u8] {
1423        &self.rom
1424    }
1425
1426    fn sram(&self) -> &[u8] {
1427        &self.bwram
1428    }
1429
1430    fn sram_mut(&mut self) -> &mut [u8] {
1431        &mut self.bwram
1432    }
1433
1434    fn irq_pending(&self) -> bool {
1435        (self.io.cpu_irqen && self.io.cpu_irqfl) || (self.io.chdma_irqen && self.io.chdma_irqfl)
1436    }
1437
1438    fn coprocessor_host_accesses(&self) -> u64 {
1439        self.host_accesses
1440    }
1441
1442    // Write the full SA-1 system state — the $2200-$23FF register file, the 2 KiB I-RAM, the H/V
1443    // timer counters, and the character-conversion DMA staging flags — into a "SA10" section. ROM
1444    // is never embedded (docs/adr/0003); BW-RAM is System::save_state's own Board::sram capture,
1445    // not duplicated here; host_accesses (a debugger counter) is excluded, matching every board.
1446    fn save_state(&self, w: &mut SaveWriter) {
1447        w.section(*b"SA10", |s| {
1448            self.io.save_state(s);
1449            s.write_u16(self.timer.scanlines);
1450            s.write_u16(self.timer.vcounter);
1451            s.write_u16(self.timer.hcounter);
1452            s.write_bool(self.reset_pending);
1453            s.write_bool(self.bwram_dma);
1454            s.write_u8(self.dma_line);
1455            s.write_bytes(&*self.iram);
1456        });
1457    }
1458
1459    fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
1460        let mut s = r.expect_section(*b"SA10")?;
1461        self.io.load_state(&mut s)?;
1462        self.timer.scanlines = s.read_u16()?;
1463        self.timer.vcounter = s.read_u16()?;
1464        self.timer.hcounter = s.read_u16()?;
1465        self.reset_pending = s.read_bool()?;
1466        self.bwram_dma = s.read_bool()?;
1467        self.dma_line = s.read_u8()? & 15; // masked identically at every normal increment
1468        self.iram.copy_from_slice(s.read_bytes(IRAM_SIZE)?);
1469        if s.remaining() != 0 {
1470            return Err(SaveStateError::Invalid(alloc::format!(
1471                "SA10 section has {} trailing byte(s)",
1472                s.remaining()
1473            )));
1474        }
1475        Ok(())
1476    }
1477
1478    // --- Second-CPU (SA-1 65C816) hooks; core owns the CPU + steps it via these. ------------
1479
1480    fn has_second_cpu(&self) -> bool {
1481        true
1482    }
1483
1484    fn second_cpu_running(&self) -> bool {
1485        !(self.io.sa1_rdyb || self.io.sa1_resb)
1486    }
1487
1488    fn second_cpu_take_reset(&mut self) -> bool {
1489        core::mem::take(&mut self.reset_pending)
1490    }
1491
1492    fn second_cpu_poll_nmi(&mut self) -> bool {
1493        if self.io.sa1_nmi && !self.io.sa1_nmicl {
1494            self.io.sa1_nmifl = true;
1495            self.io.sa1_nmicl = true;
1496            return true;
1497        }
1498        false
1499    }
1500
1501    fn second_cpu_poll_irq(&self) -> bool {
1502        (self.io.timer_irqen && !self.io.timer_irqcl)
1503            || (self.io.dma_irqen && !self.io.dma_irqcl)
1504            || (self.io.sa1_irq && !self.io.sa1_irqcl)
1505    }
1506
1507    fn second_cpu_tick(&mut self, clocks: u32) {
1508        self.tick(clocks);
1509    }
1510
1511    fn second_cpu_read(&mut self, addr24: u32) -> u8 {
1512        let bank = (addr24 >> 16) & 0xFF;
1513        let addr = addr24 & 0xFFFF;
1514        // SA-1 CPU interrupt/reset vector intercept ($00:FFEA/EE/FA/FC/FE → CNV/CIV/CRV).
1515        if bank == 0x00 {
1516            match addr {
1517                0xFFEA | 0xFFFA => return self.io.cnv as u8, // NMI (native / emulation)
1518                0xFFEB | 0xFFFB => return (self.io.cnv >> 8) as u8,
1519                0xFFEE | 0xFFFE => return self.io.civ as u8, // IRQ/BRK (native / emulation)
1520                0xFFEF | 0xFFFF => return (self.io.civ >> 8) as u8,
1521                0xFFFC => return self.io.crv as u8, // reset
1522                0xFFFD => return (self.io.crv >> 8) as u8,
1523                _ => {}
1524            }
1525        }
1526        // $2200-$23FF — SA-1-side registers.
1527        if addr24 & 0x40_fe00 == 0x00_2200 {
1528            return self.read_io_sa1(addr24);
1529        }
1530        // ROM: $00-$3F/$80-$BF:$8000-$FFFF and $C0-$FF:$0000-$FFFF.
1531        if addr24 & 0x40_8000 == 0x00_8000 || addr24 & 0xc0_0000 == 0xc0_0000 {
1532            return self.rom_read_sa1(addr24);
1533        }
1534        // BW-RAM: $00-$3F/$80-$BF:$6000-$7FFF, $40-$5F, $60-$6F.
1535        if addr24 & 0x40_e000 == 0x00_6000
1536            || addr24 & 0xe0_0000 == 0x40_0000
1537            || addr24 & 0xf0_0000 == 0x60_0000
1538        {
1539            return self.bwram_read_sa1(addr24);
1540        }
1541        // I-RAM: $00-$3F/$80-$BF:$0000-$07FF and $3000-$37FF.
1542        if addr24 & 0x40_f800 == 0x00_0000 || addr24 & 0x40_f800 == 0x00_3000 {
1543            return self.iram_read(addr24 & 0x7FF);
1544        }
1545        0
1546    }
1547
1548    fn second_cpu_write(&mut self, addr24: u32, val: u8) {
1549        if addr24 & 0x40_fe00 == 0x00_2200 {
1550            self.write_io_sa1(addr24, val);
1551            return;
1552        }
1553        if addr24 & 0x40_8000 == 0x00_8000 || addr24 & 0xc0_0000 == 0xc0_0000 {
1554            return; // ROM is read-only.
1555        }
1556        if addr24 & 0x40_e000 == 0x00_6000
1557            || addr24 & 0xe0_0000 == 0x40_0000
1558            || addr24 & 0xf0_0000 == 0x60_0000
1559        {
1560            self.bwram_write_sa1(addr24, val);
1561            return;
1562        }
1563        if addr24 & 0x40_f800 == 0x00_0000 || addr24 & 0x40_f800 == 0x00_3000 {
1564            self.iram_write_sa1(addr24 & 0x7FF, val);
1565        }
1566    }
1567}
1568
1569/// Select an SA-1 board for `rom`, sizing BW-RAM from `sram_size` and the timer from `region`.
1570#[must_use]
1571pub fn select(rom: Box<[u8]>, sram_size: usize, region: Region) -> Sa1Board {
1572    Sa1Board::new(rom, sram_size, region)
1573}
1574
1575#[cfg(test)]
1576mod tests {
1577    use super::*;
1578
1579    fn board() -> Sa1Board {
1580        // 1 MiB ROM, default BW-RAM.
1581        Sa1Board::new(vec![0u8; 0x10_0000].into_boxed_slice(), 0, Region::Ntsc)
1582    }
1583
1584    #[test]
1585    fn detects_sa1_and_has_second_cpu() {
1586        let b = board();
1587        assert_eq!(b.coprocessor(), Coprocessor::Sa1);
1588        assert!(b.has_second_cpu());
1589        // Held in reset at power-on (RESB=1).
1590        assert!(!b.second_cpu_running());
1591    }
1592
1593    #[test]
1594    fn cpu_decode_regions() {
1595        let b = board();
1596        assert!(matches!(b.map(0x00_2200), MappedAddr::Coprocessor)); // IO
1597        assert!(matches!(b.map(0x00_3000), MappedAddr::Coprocessor)); // I-RAM
1598        assert!(matches!(b.map(0x00_6000), MappedAddr::Sram(_))); // BW-RAM window
1599        assert!(matches!(b.map(0x40_0000), MappedAddr::Sram(_))); // BW-RAM linear
1600        assert!(matches!(b.map(0x00_8000), MappedAddr::Rom(_))); // ROM window
1601        assert!(matches!(b.map(0xC0_0000), MappedAddr::Rom(_))); // ROM HiROM
1602    }
1603
1604    #[test]
1605    fn reset_handshake_and_vector_intercept() {
1606        let mut b = board();
1607        // Program the SA-1 reset vector via CRV ($2203/$2204), then clear RESB ($2200 bit5=0).
1608        b.write24(0x00_2203, 0x34);
1609        b.write24(0x00_2204, 0x12);
1610        b.write24(0x00_2200, 0x00); // RESB 1->0
1611        assert!(b.second_cpu_take_reset());
1612        assert!(!b.second_cpu_take_reset()); // edge consumed
1613        assert!(b.second_cpu_running());
1614        // The SA-1 CPU reset vector fetch ($00:FFFC/D) returns CRV.
1615        assert_eq!(b.second_cpu_read(0x00_FFFC), 0x34);
1616        assert_eq!(b.second_cpu_read(0x00_FFFD), 0x12);
1617    }
1618
1619    #[test]
1620    fn arithmetic_unit_mul_div() {
1621        let mut b = board();
1622        // The arithmetic unit ($2250-$2254) is SA-1-side: written via the second CPU.
1623        // signed multiply: MD=0, ACM=0. MA=0x0010, MB=0x0010 -> 0x100.
1624        b.second_cpu_write(0x00_2250, 0x00);
1625        b.second_cpu_write(0x00_2251, 0x10);
1626        b.second_cpu_write(0x00_2252, 0x00);
1627        b.second_cpu_write(0x00_2253, 0x10);
1628        b.second_cpu_write(0x00_2254, 0x00); // trigger
1629        assert_eq!(b.second_cpu_read(0x00_2306), 0x00);
1630        assert_eq!(b.second_cpu_read(0x00_2307), 0x01); // 0x100
1631        // unsigned divide: MD=1. MA=100, MB=7 -> q=14 r=2.
1632        b.second_cpu_write(0x00_2250, 0x01);
1633        b.second_cpu_write(0x00_2251, 100);
1634        b.second_cpu_write(0x00_2252, 0x00);
1635        b.second_cpu_write(0x00_2253, 7);
1636        b.second_cpu_write(0x00_2254, 0x00);
1637        assert_eq!(b.second_cpu_read(0x00_2306), 14); // quotient low
1638        assert_eq!(b.second_cpu_read(0x00_2308), 2); // remainder low (mr>>16)
1639    }
1640
1641    #[test]
1642    fn iram_roundtrip_both_views() {
1643        let mut b = board();
1644        // S-CPU writes need the write-protect open (SIWP all blocks enabled).
1645        b.write24(0x00_2229, 0xFF);
1646        b.write24(0x00_3010, 0x5A);
1647        assert_eq!(b.read24(0x00_3010), 0x5A);
1648        // SA-1 view sees the same I-RAM at $3000-$37FF.
1649        assert_eq!(b.second_cpu_read(0x00_3010), 0x5A);
1650    }
1651
1652    #[test]
1653    fn bwram_roundtrip_main_view() {
1654        let mut b = board();
1655        // Enable S-CPU BW-RAM writes (SWEN bit7).
1656        b.write24(0x00_2226, 0x80);
1657        b.write24(0x40_0123, 0x77); // linear
1658        assert_eq!(b.read24(0x40_0123), 0x77);
1659    }
1660
1661    #[test]
1662    fn rom_window_reads_image() {
1663        let mut rom = vec![0u8; 0x10_0000];
1664        rom[0x0000] = 0xAA;
1665        rom[0x8000] = 0xBB;
1666        let mut b = Sa1Board::new(rom.into_boxed_slice(), 0, Region::Ntsc);
1667        // $00:$8000 -> block C, within 0 -> ROM 0.
1668        assert_eq!(b.read24(0x00_8000), 0xAA);
1669        // $01:$8000 -> within 0x8000 -> ROM 0x8000.
1670        assert_eq!(b.read24(0x01_8000), 0xBB);
1671    }
1672
1673    #[test]
1674    fn system_state_round_trips_through_save_state() {
1675        let mut b = board();
1676        b.write24(0x00_2203, 0x34); // CRV low
1677        b.write24(0x00_2204, 0x12); // CRV high
1678        b.write24(0x00_2200, 0x00); // RESB 1->0 (armed reset_pending)
1679        b.write24(0x00_2229, 0xFF); // SIWP open
1680        b.write24(0x00_3010, 0x5A); // I-RAM byte
1681
1682        let mut w = SaveWriter::new();
1683        b.save_state(&mut w);
1684        let bytes = w.into_bytes();
1685
1686        let mut fresh = board();
1687        let mut r = SaveReader::new(&bytes);
1688        fresh.load_state(&mut r).unwrap();
1689
1690        assert_eq!(fresh.second_cpu_read(0x00_FFFC), 0x34);
1691        assert_eq!(fresh.second_cpu_read(0x00_FFFD), 0x12);
1692        assert!(fresh.second_cpu_take_reset());
1693        assert_eq!(fresh.read24(0x00_3010), 0x5A);
1694        assert_eq!(r.remaining(), 0);
1695    }
1696}