Skip to main content

rustysnes_cart/coproc/
gsu.rs

1//! The GSU (Argonaut RISC) — the Super FX coprocessor core.
2//!
3//! The GSU is a 16-bit RISC engine that runs a program out of the Game Pak ROM (or RAM) and
4//! plots into a bitmap held in the Game Pak RAM. It powers Star Fox, Stunt Race FX, Vortex
5//! (GSU-1, ~10.74 MHz) and Yoshi's Island, Doom (GSU-2, ~21.47 MHz). Unlike the NEC DSP family
6//! there is **no chip-ROM dump** — the GSU's program lives in the cartridge ROM the user already
7//! owns, so the core is functional the moment a Super FX cart loads (`docs/cart.md`).
8//!
9//! This is a clean-room re-implementation of ares' `GSU` + `SuperFX` components (ISC) in safe
10//! `no_std` Rust. The instruction encoding, the ALT-prefix mode machine, the pixel-plot pipeline,
11//! the 256-byte/32-line opcode cache, and the ROM/RAM buffer latency are hardware facts; the
12//! decode here mirrors the published Super FX instruction set, not ares' source layout.
13//!
14//! ## Host-synchronization model (no free-running scheduler tick)
15//!
16//! The GSU is started by the SNES CPU writing the high byte of R15 (the program counter) at
17//! `$301F`, which sets the **Go** flag and begins execution at `(PBR:R15)`. The chip then runs
18//! autonomously until it executes `STOP`, which clears Go and (optionally) raises the cart IRQ.
19//! Software polls the status flag register (SFR, `$3030/$3031`) for Go to clear. Because Go is
20//! the only observable coupling between the two clocks — exactly the RQM role the DSP-1 uses —
21//! the board runs the GSU **to completion the instant Go is set** ([`Gsu::run_until_stopped`]),
22//! capped against a runaway program. This keeps the bus boundary byte-exact and fully
23//! deterministic (`docs/adr/0004`) without a per-master-clock core hook, mirroring the DSP-1
24//! `run_until_rqm` pattern. While Go is set the GSU owns the shared ROM/RAM (the CPU sees the
25//! snooze vector / open bus — see [`crate::coproc::superfx`]); run-to-completion serialises that
26//! arbitration naturally.
27
28// The GSU aliases every register as signed and unsigned, the status register is a bitfield of
29// single-bit flags, and the plot/character addressing is dense with deliberate narrowing casts.
30// Flagging each would bury real issues, so the cast family + bitfield lints are allowed here.
31#![allow(
32    clippy::cast_possible_truncation,
33    clippy::cast_possible_wrap,
34    clippy::cast_sign_loss,
35    clippy::cast_lossless,
36    clippy::doc_markdown,
37    clippy::similar_names,
38    clippy::unreadable_literal,
39    clippy::struct_excessive_bools,
40    clippy::missing_const_for_fn,
41    clippy::verbose_bit_mask,
42    clippy::if_not_else
43)]
44
45use alloc::boxed::Box;
46use alloc::vec;
47use alloc::vec::Vec;
48
49use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
50
51/// The Game Pak ROM/RAM the GSU reads and plots into, with the GSU-internal 24-bit decode.
52///
53/// The board constructs this each run, borrowing its ROM (shared, read-only) and its Game Pak
54/// RAM (the plot target). The address map is the GSU's own view (ares `SuperFX::read`/`write`):
55/// banks `$00-$3F` fold LoROM-style, `$40-$5F` are linear ROM, `$70-$71` are the RAM bitmap.
56pub struct GsuMem<'a> {
57    /// Game Pak ROM image (already mirror-rounded to a power-of-two `rom_mask + 1`).
58    pub rom: &'a [u8],
59    /// `rom.len().next_power_of_two() - 1` (ares `romSizeRound(rom.size()) - 1`).
60    pub rom_mask: u32,
61    /// Game Pak RAM (the GSU plot bitmap), `ram_mask + 1` bytes.
62    pub ram: &'a mut [u8],
63    /// `ram.len() - 1` (RAM is always a power of two).
64    pub ram_mask: u32,
65}
66
67impl GsuMem<'_> {
68    /// GSU-internal read of a 24-bit address (ROM banks `$00-$5F`, RAM banks `$70-$71`).
69    #[must_use]
70    fn read(&self, address: u32) -> u8 {
71        if address & 0xc0_0000 == 0x00_0000 {
72            // $00-3F:any — LoROM fold: ((A & $3F0000) >> 1) | (A & $7FFF).
73            let off = (((address & 0x3f_0000) >> 1) | (address & 0x7fff)) & self.rom_mask;
74            return self.rom.get(off as usize).copied().unwrap_or(0);
75        }
76        if address & 0xe0_0000 == 0x40_0000 {
77            // $40-5F:0000-FFFF — linear ROM.
78            let off = address & self.rom_mask;
79            return self.rom.get(off as usize).copied().unwrap_or(0);
80        }
81        if address & 0xfe_0000 == 0x70_0000 {
82            // $70-71:0000-FFFF — Game Pak RAM.
83            let off = address & self.ram_mask;
84            return self.ram.get(off as usize).copied().unwrap_or(0);
85        }
86        0
87    }
88
89    /// GSU-internal write — only the Game Pak RAM (`$70-$71`) is writable.
90    fn write(&mut self, address: u32, data: u8) {
91        if address & 0xfe_0000 == 0x70_0000 {
92            let off = (address & self.ram_mask) as usize;
93            if let Some(slot) = self.ram.get_mut(off) {
94                *slot = data;
95            }
96        }
97    }
98}
99
100// --- Status flag register (SFR) bit masks (ares `registers.hpp::SFR`). ---------------------
101
102/// Zero flag.
103const SFR_Z: u16 = 1 << 1;
104/// Carry flag.
105const SFR_CY: u16 = 1 << 2;
106/// Sign flag.
107const SFR_S: u16 = 1 << 3;
108/// Overflow flag.
109const SFR_OV: u16 = 1 << 4;
110/// Go flag — the GSU is running.
111const SFR_G: u16 = 1 << 5;
112/// ROM-buffer (R14) read pending flag.
113const SFR_R: u16 = 1 << 6;
114/// ALT1 instruction-mode prefix.
115const SFR_ALT1: u16 = 1 << 8;
116/// ALT2 instruction-mode prefix.
117const SFR_ALT2: u16 = 1 << 9;
118/// WITH (register-pair) prefix flag.
119const SFR_B: u16 = 1 << 12;
120/// Interrupt-request flag (raised by STOP).
121const SFR_IRQ: u16 = 1 << 15;
122/// The host-visible SFR read mask (ares `SFR::operator u32`).
123const SFR_READ_MASK: u16 = 0x9f7e;
124
125/// The two-deep pixel cache (ares `PixelCache`): one 8x1 strip of pending plot colours.
126#[derive(Clone, Copy)]
127struct PixelCache {
128    /// Character-strip offset `(y << 5) + (x >> 3)`; `!0` = empty.
129    offset: u16,
130    /// Per-column bit-pending mask (bit set = column written since the strip opened).
131    bitpend: u8,
132    /// The 8 column colours (index `(x & 7) ^ 7`).
133    data: [u8; 8],
134}
135
136impl PixelCache {
137    const fn empty() -> Self {
138        Self {
139            offset: !0,
140            bitpend: 0,
141            data: [0; 8],
142        }
143    }
144}
145
146/// The Super FX GSU core — registers, status, caches, and the ROM/RAM buffer latches.
147///
148/// The board owns the ROM/RAM bytes and supplies them via [`GsuMem`] on each run; this struct is
149/// the pure register/state machine (so the whole thing is `Clone` for save-states).
150pub struct Gsu {
151    /// R0-R15 general-purpose registers (R15 = program counter).
152    r: [u16; 16],
153    /// `true` when R14 was written this instruction (triggers a ROM-buffer fetch).
154    r14_mod: bool,
155    /// `true` when R15 was written this instruction (an explicit PC change — no auto-increment).
156    r15_mod: bool,
157    /// Status flag register.
158    sfr: u16,
159    /// Source-register select (set by FROM/WITH prefixes).
160    sreg: usize,
161    /// Destination-register select (set by TO/WITH prefixes).
162    dreg: usize,
163    /// The prefetched opcode (the 1-instruction pipeline — gives the GSU its branch delay slot).
164    pipeline: u8,
165    /// Program bank register.
166    pbr: u8,
167    /// ROM bank register (R14-relative ROM-buffer bank).
168    rombr: u8,
169    /// RAM bank register (0/1).
170    rambr: bool,
171    /// Cache base register.
172    cbr: u16,
173    /// Screen base register (the plot bitmap base, `<< 10`).
174    scbr: u8,
175    /// Screen-mode register: height select + ROM/RAM-on + colour depth.
176    scmr_ht: u8,
177    /// SCMR ROM-on: GSU owns the ROM bus.
178    scmr_ron: bool,
179    /// SCMR RAM-on: GSU owns the RAM bus.
180    scmr_ran: bool,
181    /// SCMR colour-depth mode (0/1/2/3 → 2/4/4/8 bpp).
182    scmr_md: u8,
183    /// Colour register (the plot colour).
184    colr: u8,
185    /// Plot-option register: OBJ mode, freeze-high, high-nibble, dither, transparent.
186    por_obj: bool,
187    por_freezehigh: bool,
188    por_highnibble: bool,
189    por_dither: bool,
190    por_transparent: bool,
191    /// Backup-RAM write-enable register.
192    bramr: bool,
193    /// Version code register.
194    vcr: u8,
195    /// CFGR IRQ-mask bit (when set, STOP does not raise IRQ).
196    cfgr_irq: bool,
197    /// CFGR MS0 fast-multiply bit.
198    cfgr_ms0: bool,
199    /// Clock-select register (`true` = 21 MHz; halves access latencies).
200    clsr: bool,
201    /// Clocks until the ROM data register (`romdr`) is valid.
202    romcl: u32,
203    /// ROM-buffer data register (the byte at `(ROMBR:R14)`).
204    romdr: u8,
205    /// Clocks until the pending RAM-buffer write commits.
206    ramcl: u32,
207    /// RAM-buffer address register (the pending write address).
208    ramar: u16,
209    /// RAM-buffer data register (the pending write byte).
210    ramdr: u8,
211    /// The last RAM address touched by a load/store (ares `regs.ramaddr`).
212    ramaddr: u16,
213    /// 512-byte opcode cache buffer (32 lines x 16 bytes).
214    cache_buffer: Box<[u8; 512]>,
215    /// Per-line cache-valid flags.
216    cache_valid: [bool; 32],
217    /// The two-deep plot pixel cache.
218    pixelcache: [PixelCache; 2],
219    /// Cumulative GSU clock ticks (for the timing model + the runaway cap).
220    clocks: u64,
221    /// Cumulative instructions executed (the liveness counter the board exposes).
222    instructions: u64,
223    /// Per-bus-access clock checkpoints queued by [`Gsu::step`] within the instruction
224    /// currently being drained by [`Gsu::step_one`]; `pending_idx` is the next unread entry
225    /// (an index cursor avoids an O(n) pop-front on every access).
226    pending_clocks: Vec<u32>,
227    /// Read cursor into `pending_clocks`.
228    pending_idx: usize,
229    /// Master clocks still owed on the checkpoint most recently pulled by [`Gsu::tick`] before
230    /// the next one may be pulled — the countdown that paces per-access checkpoints out one
231    /// master clock at a time (mirrors the `romcl`/`ramcl` delayed-commit pattern, generalized
232    /// to gate overall instruction progress rather than just ROM/RAM buffer latches). The
233    /// instruction's memory side effects are NOT deferred to when this reaches zero — they
234    /// already happened, eagerly, the moment [`Gsu::step_one`] pulled the checkpoint; `owed`
235    /// only withholds the checkpoint *after* it (i.e. gates how soon the GSU can make its next
236    /// move), which is the piece that lets the CPU's own instructions interleave in between.
237    owed: u32,
238}
239
240impl Clone for Gsu {
241    fn clone(&self) -> Self {
242        Self {
243            cache_buffer: self.cache_buffer.clone(),
244            pending_clocks: self.pending_clocks.clone(),
245            ..*self
246        }
247    }
248}
249
250impl core::fmt::Debug for Gsu {
251    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
252        f.debug_struct("Gsu")
253            .field("go", &self.go())
254            .field("pbr", &self.pbr)
255            .field("r15", &self.r[15])
256            .field("instructions", &self.instructions)
257            .finish_non_exhaustive()
258    }
259}
260
261impl Default for Gsu {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267impl Gsu {
268    /// Power-on a GSU core (ares `GSU::power` + `SuperFX::power`).
269    ///
270    /// # Panics
271    /// Never in practice: the only fallible step is sizing the fixed 512-byte cache buffer, which
272    /// cannot fail for the constant length.
273    #[must_use]
274    pub fn new() -> Self {
275        Self {
276            r: [0; 16],
277            r14_mod: false,
278            r15_mod: false,
279            sfr: 0,
280            sreg: 0,
281            dreg: 0,
282            pbr: 0,
283            rombr: 0,
284            rambr: false,
285            cbr: 0,
286            scbr: 0,
287            scmr_ht: 0,
288            scmr_ron: false,
289            scmr_ran: false,
290            scmr_md: 0,
291            colr: 0,
292            por_obj: false,
293            por_freezehigh: false,
294            por_highnibble: false,
295            por_dither: false,
296            por_transparent: false,
297            bramr: false,
298            vcr: 0x04,
299            cfgr_irq: false,
300            cfgr_ms0: false,
301            clsr: false,
302            romcl: 0,
303            romdr: 0,
304            ramcl: 0,
305            ramar: 0,
306            ramdr: 0,
307            ramaddr: 0,
308            pipeline: 0x01, // nop
309            cache_buffer: vec![0u8; 512].into_boxed_slice().try_into().unwrap(),
310            cache_valid: [false; 32],
311            pixelcache: [PixelCache::empty(); 2],
312            clocks: 0,
313            instructions: 0,
314            pending_clocks: Vec::new(),
315            pending_idx: 0,
316            owed: 0,
317        }
318    }
319
320    /// Whether the Go flag is set (the GSU is running / owns the bus).
321    #[must_use]
322    pub const fn go(&self) -> bool {
323        self.sfr & SFR_G != 0
324    }
325
326    /// Whether the GSU currently owns the ROM bus (Go + SCMR ROM-on).
327    #[must_use]
328    pub const fn owns_rom(&self) -> bool {
329        self.go() && self.scmr_ron
330    }
331
332    /// Whether the GSU currently owns the RAM bus (Go + SCMR RAM-on).
333    #[must_use]
334    pub const fn owns_ram(&self) -> bool {
335        self.go() && self.scmr_ran
336    }
337
338    /// Whether the GSU is asserting its IRQ line (SFR IRQ flag, gated by CFGR IRQ-mask).
339    #[must_use]
340    pub const fn irq_pending(&self) -> bool {
341        self.sfr & SFR_IRQ != 0
342    }
343
344    /// The number of instructions executed since power-on.
345    #[must_use]
346    pub const fn instructions(&self) -> u64 {
347        self.instructions
348    }
349
350    /// The number of GSU clock cycles executed.
351    #[must_use]
352    pub const fn clocks(&self) -> u64 {
353        self.clocks
354    }
355
356    // --- SFR helpers. ----------------------------------------------------------------------
357
358    #[inline]
359    const fn flag(&self, mask: u16) -> bool {
360        self.sfr & mask != 0
361    }
362
363    #[inline]
364    fn set_flag(&mut self, mask: u16, value: bool) {
365        if value {
366            self.sfr |= mask;
367        } else {
368            self.sfr &= !mask;
369        }
370    }
371
372    /// The composite ALT mode (alt1 | alt2 << 1) ares uses for the prefixed-instruction split.
373    #[inline]
374    const fn alt1(&self) -> bool {
375        self.flag(SFR_ALT1)
376    }
377    #[inline]
378    const fn alt2(&self) -> bool {
379        self.flag(SFR_ALT2)
380    }
381
382    // --- Register write with R14/R15 modified tracking (ares `Register::modified`). --------
383
384    #[inline]
385    fn write_r(&mut self, n: usize, val: u16) {
386        self.r[n] = val;
387        match n {
388            14 => self.r14_mod = true,
389            15 => self.r15_mod = true,
390            _ => {}
391        }
392    }
393
394    /// Source register (FROM / WITH selected).
395    #[inline]
396    const fn sr(&self) -> u16 {
397        self.r[self.sreg]
398    }
399
400    /// Write the destination register (TO / WITH selected).
401    #[inline]
402    fn set_dr(&mut self, val: u16) {
403        let d = self.dreg;
404        self.write_r(d, val);
405    }
406
407    /// Per-instruction prefix reset (ares `Registers::reset` — NOT power-on).
408    #[inline]
409    fn reset_prefix(&mut self) {
410        self.sfr &= !(SFR_B | SFR_ALT1 | SFR_ALT2);
411        self.sreg = 0;
412        self.dreg = 0;
413    }
414
415    // --- Debug-only read accessors (no side effects, unlike `read_register`'s memory-mapped
416    // window which can have read-clear/latch behavior on some addresses). For the debugger
417    // overlay's Cart panel (`docs/frontend.md` §Debugger overlay). ------------------------------
418
419    /// The R0-R15 general-purpose register file (R15 is also the program counter).
420    #[must_use]
421    pub const fn registers(&self) -> [u16; 16] {
422        self.r
423    }
424
425    /// The status flag register (SFR).
426    #[must_use]
427    pub const fn sfr(&self) -> u16 {
428        self.sfr
429    }
430
431    /// The program bank register.
432    #[must_use]
433    pub const fn pbr(&self) -> u8 {
434        self.pbr
435    }
436
437    // --- The host-sync driver. -------------------------------------------------------------
438
439    /// Drain one bus-access clock checkpoint (ares `SuperFX::step`/`Thread::synchronize`
440    /// granularity — `sfc/coprocessor/superfx/timing.cpp`) and report its clock count, running
441    /// as many further GSU instructions as needed to produce one. The board's host loop calls
442    /// this in a loop, folding each returned delta into the master clock immediately, so the
443    /// PPU/NMI/APU advance in step with the GSU at true per-access granularity instead of
444    /// crediting an entire instruction — or an entire multi-instruction burst — all at once
445    /// (which let a two-pass render, e.g. a screen split into a left-half then a right-half
446    /// `Go` burst, desync the two halves' notion of "now"). Each instruction still runs to
447    /// completion eagerly inside `Gsu::main_step` — only the *reporting* of its accesses to
448    /// the caller is paced one at a time, which is externally indistinguishable from true
449    /// per-access interleaving because nothing GSU-side reads back caller/Bus state
450    /// mid-instruction. Returns `0` once `Go` clears and there is nothing left queued.
451    pub fn step_one(&mut self, mem: &mut GsuMem) -> u32 {
452        loop {
453            if self.pending_idx < self.pending_clocks.len() {
454                let clocks = self.pending_clocks[self.pending_idx];
455                self.pending_idx += 1;
456                if self.pending_idx == self.pending_clocks.len() {
457                    self.pending_clocks.clear();
458                    self.pending_idx = 0;
459                }
460                if clocks > 0 {
461                    return clocks;
462                }
463                continue;
464            }
465            if !self.go() {
466                return 0;
467            }
468            self.main_step(mem);
469        }
470    }
471
472    /// Advance by exactly one master clock (ares `SuperFX::main`, scheduled by its `Thread` at
473    /// native master-clock granularity — `sfc/coprocessor/superfx/superfx.cpp`). The board's
474    /// host loop calls this from inside the Bus's own per-master-tick loop, unconditionally,
475    /// every single tick — the same place the PPU dot and the APU's SMP-cycle release happen —
476    /// so the GSU genuinely interleaves with the CPU's own instruction stream instead of a `Go`
477    /// burst draining to completion "atomically" inside the one bus write that armed it (which
478    /// left the CPU unable to do any of its own work, or service a second `Go` burst, until the
479    /// first fully finished). `owed` paces [`Gsu::step_one`]'s per-access checkpoints out one
480    /// master clock at a time: the instruction's own side effects still land eagerly the moment
481    /// its checkpoint is pulled (no full deferred-commit — see the field doc), but the *next*
482    /// GSU instruction can no longer start until the real number of elapsed master clocks the
483    /// current one costs has actually passed, letting the CPU run freely in between.
484    pub fn tick(&mut self, mem: &mut GsuMem) {
485        if self.owed > 0 {
486            self.owed -= 1;
487            return;
488        }
489        let clocks = self.step_one(mem);
490        if clocks > 0 {
491            self.owed = clocks - 1;
492        }
493    }
494
495    /// Run the GSU to completion: step instructions while Go is set, capped against a runaway
496    /// program. Called by the board the instant the CPU sets Go (the DSP-1 `run_until_rqm`
497    /// analogue). Returns when the program executes `STOP` (Go clears) or the cap trips.
498    ///
499    /// Prefer [`Gsu::step_one`] driven by the board's host loop when master-clock-accurate
500    /// interleaving matters (e.g. a screen render split across multiple `Go` bursts within one
501    /// displayed frame) — this method is for callers that only need the final result.
502    pub fn run_until_stopped(&mut self, mem: &mut GsuMem) {
503        /// Instruction cap: a generous bound (~30 frames of GSU work) so a wedged program can't
504        /// spin the host forever. A correct program reaches `STOP` long before this.
505        const MAX_INSTRUCTIONS: u64 = 8_000_000;
506        let start = self.instructions;
507        while self.go() && self.instructions - start < MAX_INSTRUCTIONS {
508            self.main_step(mem);
509        }
510        // Nobody drains the per-access checkpoints this run pushed (only `step_one`'s callers
511        // want them) — clear them so they don't accumulate across repeated calls.
512        self.pending_clocks.clear();
513        self.pending_idx = 0;
514    }
515
516    /// Execute one GSU instruction (ares `SuperFX::main`, the Go-set path).
517    fn main_step(&mut self, mem: &mut GsuMem) {
518        let opcode = self.peekpipe(mem);
519        self.instructions = self.instructions.wrapping_add(1);
520        self.execute(opcode, mem);
521
522        if self.r14_mod {
523            self.r14_mod = false;
524            self.update_rom_buffer();
525        }
526        if self.r15_mod {
527            self.r15_mod = false;
528        } else {
529            self.r[15] = self.r[15].wrapping_add(1);
530        }
531    }
532
533    // --- Timing / ROM-RAM buffer latency (ares `timing.cpp`). ------------------------------
534
535    /// Advance the GSU clock by `clocks`, committing the ROM/RAM buffer when their latency
536    /// elapses.
537    fn step(&mut self, clocks: u32, mem: &mut GsuMem) {
538        if self.romcl != 0 {
539            self.romcl -= clocks.min(self.romcl);
540            if self.romcl == 0 {
541                self.set_flag(SFR_R, false);
542                self.romdr = mem.read((u32::from(self.rombr) << 16) + u32::from(self.r[14]));
543            }
544        }
545        if self.ramcl != 0 {
546            self.ramcl -= clocks.min(self.ramcl);
547            if self.ramcl == 0 {
548                let addr = 0x70_0000 + (u32::from(self.rambr) << 16) + u32::from(self.ramar);
549                mem.write(addr, self.ramdr);
550            }
551        }
552        self.clocks = self.clocks.wrapping_add(u64::from(clocks));
553        // Queue this access's clocks as a synchronization checkpoint (ares `SuperFX::step`
554        // calls `Thread::synchronize(cpu)` at exactly this point, every single bus access —
555        // `sfc/coprocessor/superfx/timing.cpp`). `step_one` drains one checkpoint per call so
556        // the board's host loop can fold each one into the master clock immediately instead of
557        // crediting an entire instruction (or an entire multi-instruction burst) at once. The
558        // instruction still runs to completion eagerly here — only the *reporting* of its bus
559        // accesses to the Bus is deferred/paced, which is externally indistinguishable from true
560        // per-access interleaving because nothing GSU-side reads back Bus state mid-instruction.
561        self.pending_clocks.push(clocks);
562    }
563
564    /// Latency for a normal (non-cache-hit) clocked access — halved at 21 MHz (CLSR).
565    #[inline]
566    const fn access_latency(&self) -> u32 {
567        if self.clsr { 5 } else { 6 }
568    }
569
570    fn sync_rom_buffer(&mut self, mem: &mut GsuMem) {
571        if self.romcl != 0 {
572            self.step(self.romcl, mem);
573        }
574    }
575
576    fn read_rom_buffer(&mut self, mem: &mut GsuMem) -> u8 {
577        self.sync_rom_buffer(mem);
578        self.romdr
579    }
580
581    fn update_rom_buffer(&mut self) {
582        self.set_flag(SFR_R, true);
583        self.romcl = self.access_latency();
584    }
585
586    fn sync_ram_buffer(&mut self, mem: &mut GsuMem) {
587        if self.ramcl != 0 {
588            self.step(self.ramcl, mem);
589        }
590    }
591
592    fn read_ram_buffer(&mut self, mem: &mut GsuMem, address: u16) -> u8 {
593        self.sync_ram_buffer(mem);
594        mem.read(0x70_0000 + (u32::from(self.rambr) << 16) + u32::from(address))
595    }
596
597    fn write_ram_buffer(&mut self, mem: &mut GsuMem, address: u16, data: u8) {
598        self.sync_ram_buffer(mem);
599        self.ramcl = self.access_latency();
600        self.ramar = address;
601        self.ramdr = data;
602    }
603
604    // --- Opcode fetch + cache (ares `memory.cpp`). -----------------------------------------
605
606    fn read_opcode(&mut self, address: u16, mem: &mut GsuMem) -> u8 {
607        let offset = address.wrapping_sub(self.cbr);
608        if offset < 512 {
609            let line = (offset >> 4) as usize;
610            if !self.cache_valid[line] {
611                let dp0 = (offset & 0xfff0) as usize;
612                let sp0 = (u32::from(self.pbr) << 16)
613                    + u32::from(self.cbr.wrapping_add(dp0 as u16) & 0xfff0);
614                for i in 0..16 {
615                    self.step(self.access_latency(), mem);
616                    self.cache_buffer[dp0 + i] = mem.read(sp0 + i as u32);
617                }
618                self.cache_valid[line] = true;
619            } else {
620                self.step(if self.clsr { 1 } else { 2 }, mem);
621            }
622            return self.cache_buffer[offset as usize];
623        }
624
625        if self.pbr <= 0x5f {
626            self.sync_rom_buffer(mem);
627        } else {
628            self.sync_ram_buffer(mem);
629        }
630        self.step(self.access_latency(), mem);
631        mem.read((u32::from(self.pbr) << 16) | u32::from(address))
632    }
633
634    /// Fetch the next opcode without advancing R15 (ares `peekpipe`).
635    fn peekpipe(&mut self, mem: &mut GsuMem) -> u8 {
636        let result = self.pipeline;
637        self.pipeline = self.read_opcode(self.r[15], mem);
638        self.r15_mod = false;
639        result
640    }
641
642    /// Consume an operand byte, advancing R15 (ares `pipe`).
643    fn pipe(&mut self, mem: &mut GsuMem) -> u8 {
644        let result = self.pipeline;
645        self.r[15] = self.r[15].wrapping_add(1);
646        self.pipeline = self.read_opcode(self.r[15], mem);
647        self.r15_mod = false;
648        result
649    }
650
651    fn flush_cache(&mut self) {
652        self.cache_valid = [false; 32];
653    }
654
655    /// Host read/write of the cache window (`$3100-$32FF`, ares `readCache`/`writeCache`).
656    fn read_cache(&self, address: u16) -> u8 {
657        self.cache_buffer[(address.wrapping_add(self.cbr) & 511) as usize]
658    }
659
660    fn write_cache(&mut self, address: u16, data: u8) {
661        let a = address.wrapping_add(self.cbr) & 511;
662        self.cache_buffer[a as usize] = data;
663        if a & 15 == 15 {
664            self.cache_valid[(a >> 4) as usize] = true;
665        }
666    }
667
668    // --- Plot / colour / rpix pipeline (ares `core.cpp`). ----------------------------------
669
670    /// Apply the colour-register transform for `source` (ares `SuperFX::color`).
671    const fn color(&self, source: u8) -> u8 {
672        if self.por_highnibble {
673            return (self.colr & 0xf0) | (source >> 4);
674        }
675        if self.por_freezehigh {
676            return (self.colr & 0xf0) | (source & 0x0f);
677        }
678        source
679    }
680
681    /// Read back the plotted pixel at `(x, y)` (ares `SuperFX::rpix`); also flushes both caches.
682    fn rpix(&mut self, x: u8, y: u8, mem: &mut GsuMem) -> u8 {
683        // ares flushes both strips in place (clearing their bitpend so they are not re-flushed).
684        // We flush by value, then clear the originals to match.
685        let pc1 = self.pixelcache[1];
686        self.flush_pixel_cache_into(pc1, mem);
687        self.pixelcache[1].bitpend = 0;
688        let pc0 = self.pixelcache[0];
689        self.flush_pixel_cache_into(pc0, mem);
690        self.pixelcache[0].bitpend = 0;
691
692        let bpp = Self::bpp(self.scmr_md);
693        let addr = self.char_address(x, y, bpp);
694        let mut data = 0u8;
695        let xi = (x & 7) ^ 7;
696        for n in 0..bpp {
697            let byte = ((n >> 1) << 4) + (n & 1);
698            self.step(if self.clsr { 5 } else { 6 }, mem);
699            data |= ((mem.read(addr + byte) >> xi) & 1) << n;
700        }
701        data
702    }
703
704    /// Bits-per-pixel for SCMR mode (ares `2 << (md - (md >> 1))` = {2, 4, 4, 8}).
705    const fn bpp(md: u8) -> u32 {
706        (2 << (md - (md >> 1))) as u32
707    }
708
709    /// The Game Pak RAM byte address of the character row holding `(x, y)` (ares character-number
710    /// math, shared by `plot`/`rpix`/`flushPixelCache`).
711    fn char_address(&self, x: u8, y: u8, bpp: u32) -> u32 {
712        let x = u32::from(x);
713        let y = u32::from(y);
714        let cn = match if self.por_obj { 3 } else { self.scmr_ht } {
715            0 => ((x & 0xf8) << 1) + ((y & 0xf8) >> 3),
716            1 => ((x & 0xf8) << 1) + ((x & 0xf8) >> 1) + ((y & 0xf8) >> 3),
717            2 => ((x & 0xf8) << 1) + (x & 0xf8) + ((y & 0xf8) >> 3),
718            _ => ((y & 0x80) << 2) + ((x & 0x80) << 1) + ((y & 0x78) << 1) + ((x & 0x78) >> 3),
719        };
720        0x70_0000 + (cn * (bpp << 3)) + (u32::from(self.scbr) << 10) + ((y & 0x07) * 2)
721    }
722
723    /// Flush one pixel-cache strip to the Game Pak RAM (ares `flushPixelCache`). The strip is
724    /// taken by value; callers clear the live `pixelcache` entry's `bitpend` themselves.
725    fn flush_pixel_cache_into(&mut self, cache: PixelCache, mem: &mut GsuMem) {
726        if cache.bitpend == 0 {
727            return;
728        }
729        let x = (cache.offset << 3) as u8;
730        let y = (cache.offset >> 5) as u8;
731        let bpp = Self::bpp(self.scmr_md);
732        let addr = self.char_address(x, y, bpp);
733
734        for n in 0..bpp {
735            let byte = ((n >> 1) << 4) + (n & 1);
736            let mut data = 0u8;
737            for col in 0..8 {
738                data |= ((cache.data[col] >> n) & 1) << col;
739            }
740            if cache.bitpend != 0xff {
741                self.step(if self.clsr { 5 } else { 6 }, mem);
742                data &= cache.bitpend;
743                data |= mem.read(addr + byte) & !cache.bitpend;
744            }
745            self.step(if self.clsr { 5 } else { 6 }, mem);
746            mem.write(addr + byte, data);
747        }
748    }
749
750    // --- Host register interface ($3000-$32FF), used by the board. -------------------------
751
752    /// Host read of a GSU register (`address` = `$0000-$02FF` window offset; ares `readIO`).
753    pub fn read_register(&mut self, address: u16) -> u8 {
754        let address = 0x3000 | (address & 0x1ff);
755        if (0x3100..=0x32ff).contains(&address) {
756            return self.read_cache(address - 0x3100);
757        }
758        if (0x3000..=0x301f).contains(&address) {
759            let r = self.r[(address >> 1 & 15) as usize];
760            return (r >> ((address & 1) << 3)) as u8;
761        }
762        match address {
763            0x3030 => self.sfr as u8,
764            0x3031 => {
765                let r = (self.sfr & SFR_READ_MASK) >> 8;
766                self.set_flag(SFR_IRQ, false);
767                r as u8
768            }
769            0x3034 => self.pbr,
770            0x3036 => self.rombr,
771            0x303b => self.vcr,
772            0x303c => u8::from(self.rambr),
773            0x303e => self.cbr as u8,
774            0x303f => (self.cbr >> 8) as u8,
775            _ => 0,
776        }
777    }
778
779    /// Host write of a GSU register. Returns `true` if this write set the Go flag (0->1), so the
780    /// board knows to run the GSU to completion (ares `writeIO`).
781    #[must_use]
782    pub fn write_register(&mut self, address: u16, data: u8) -> bool {
783        let address = 0x3000 | (address & 0x1ff);
784        let was_go = self.go();
785
786        if (0x3100..=0x32ff).contains(&address) {
787            self.write_cache(address - 0x3100, data);
788            return false;
789        }
790        if (0x3000..=0x301f).contains(&address) {
791            let n = (address >> 1 & 15) as usize;
792            if address & 1 == 0 {
793                self.r[n] = (self.r[n] & 0xff00) | u16::from(data);
794            } else {
795                self.r[n] = (u16::from(data) << 8) | (self.r[n] & 0x00ff);
796            }
797            if n == 14 {
798                self.update_rom_buffer();
799            }
800            if address == 0x301f {
801                self.set_flag(SFR_G, true);
802            }
803            return !was_go && self.go();
804        }
805
806        match address {
807            0x3030 => {
808                let g = self.go();
809                self.sfr = (self.sfr & 0xff00) | u16::from(data);
810                if g && !self.go() {
811                    self.cbr = 0;
812                    self.flush_cache();
813                }
814            }
815            0x3031 => self.sfr = (u16::from(data) << 8) | (self.sfr & 0x00ff),
816            0x3033 => self.bramr = data & 0x01 != 0,
817            0x3034 => {
818                self.pbr = data & 0x7f;
819                self.flush_cache();
820            }
821            0x3037 => {
822                self.cfgr_irq = data & 0x80 != 0;
823                self.cfgr_ms0 = data & 0x20 != 0;
824            }
825            0x3038 => self.scbr = data,
826            0x3039 => self.clsr = data & 0x01 != 0,
827            0x303a => self.set_scmr(data),
828            _ => {}
829        }
830        !was_go && self.go()
831    }
832
833    /// Decode the SCMR byte (ares `SCMR::operator=`).
834    fn set_scmr(&mut self, data: u8) {
835        self.scmr_ht = (u8::from(data & 0x20 != 0) << 1) | u8::from(data & 0x04 != 0);
836        self.scmr_ron = data & 0x10 != 0;
837        self.scmr_ran = data & 0x08 != 0;
838        self.scmr_md = data & 0x03;
839    }
840
841    // --- The instruction set (ares `instructions.cpp` + `instruction.cpp` switch). ---------
842
843    #[allow(clippy::too_many_lines)]
844    fn execute(&mut self, opcode: u8, mem: &mut GsuMem) {
845        let n = (opcode & 0x0f) as usize;
846        match opcode {
847            0x00 => self.i_stop(),
848            0x01 => self.reset_prefix(),      // nop
849            0x02 => self.i_cache(),           // cache
850            0x03 => self.i_lsr(),             // lsr
851            0x04 => self.i_rol(),             // rol
852            0x05 => self.i_branch(true, mem), // bra
853            0x06 => self.i_branch(self.flag(SFR_S) == self.flag(SFR_OV), mem), // bge? -> see below
854            0x07 => self.i_branch(self.flag(SFR_S) != self.flag(SFR_OV), mem),
855            0x08 => self.i_branch(!self.flag(SFR_Z), mem), // bne
856            0x09 => self.i_branch(self.flag(SFR_Z), mem),  // beq
857            0x0a => self.i_branch(!self.flag(SFR_S), mem), // bpl
858            0x0b => self.i_branch(self.flag(SFR_S), mem),  // bmi
859            0x0c => self.i_branch(!self.flag(SFR_CY), mem), // bcc
860            0x0d => self.i_branch(self.flag(SFR_CY), mem), // bcs
861            0x0e => self.i_branch(!self.flag(SFR_OV), mem), // bvc
862            0x0f => self.i_branch(self.flag(SFR_OV), mem), // bvs
863            0x10..=0x1f => self.i_to_move(n),              // to/move
864            0x20..=0x2f => self.i_with(n),                 // with
865            0x30..=0x3b => self.i_store(n, mem),           // stw/stb
866            0x3c => self.i_loop(),                         // loop
867            0x3d => self.i_alt1(),
868            0x3e => self.i_alt2(),
869            0x3f => self.i_alt3(),
870            0x40..=0x4b => self.i_load(n, mem),        // ldw/ldb
871            0x4c => self.i_plot_rpix(mem),             // plot/rpix
872            0x4d => self.i_swap(),                     // swap
873            0x4e => self.i_color_cmode(),              // color/cmode
874            0x4f => self.i_not(),                      // not
875            0x50..=0x5f => self.i_add_adc(n, mem),     // add/adc
876            0x60..=0x6f => self.i_sub_sbc_cmp(n, mem), // sub/sbc/cmp
877            0x70 => self.i_merge(),                    // merge
878            0x71..=0x7f => self.i_and_bic(n, mem),     // and/bic
879            0x80..=0x8f => self.i_mult_umult(n, mem),  // mult/umult
880            0x90 => self.i_sbk(mem),                   // sbk
881            0x91..=0x94 => self.i_link(n),             // link
882            0x95 => self.i_sex(),                      // sex
883            0x96 => self.i_asr_div2(),                 // asr/div2
884            0x97 => self.i_ror(),                      // ror
885            0x98..=0x9d => self.i_jmp_ljmp(n),         // jmp/ljmp
886            0x9e => self.i_lob(),                      // lob
887            0x9f => self.i_fmult_lmult(mem),           // fmult/lmult
888            0xa0..=0xaf => self.i_ibt_lms_sms(n, mem), // ibt/lms/sms
889            0xb0..=0xbf => self.i_from_moves(n),       // from/moves
890            0xc0 => self.i_hib(),                      // hib
891            0xc1..=0xcf => self.i_or_xor(n, mem),      // or/xor
892            0xd0..=0xde => self.i_inc(n),              // inc
893            0xdf => self.i_getc_ramb_romb(mem),        // getc/ramb/romb
894            0xe0..=0xee => self.i_dec(n),              // dec
895            0xef => self.i_getb(mem),                  // getb
896            0xf0..=0xff => self.i_iwt_lm_sm(n, mem),   // iwt/lm/sm
897        }
898    }
899
900    fn set_sz_from(&mut self, value: u16) {
901        self.set_flag(SFR_S, value & 0x8000 != 0);
902        self.set_flag(SFR_Z, value == 0);
903    }
904
905    // $00 stop
906    fn i_stop(&mut self) {
907        if !self.cfgr_irq {
908            self.set_flag(SFR_IRQ, true);
909        }
910        self.set_flag(SFR_G, false);
911        self.pipeline = 0x01;
912        self.reset_prefix();
913    }
914
915    // $02 cache
916    fn i_cache(&mut self) {
917        if self.cbr != (self.r[15] & 0xfff0) {
918            self.cbr = self.r[15] & 0xfff0;
919            self.flush_cache();
920        }
921        self.reset_prefix();
922    }
923
924    // $03 lsr
925    fn i_lsr(&mut self) {
926        self.set_flag(SFR_CY, self.sr() & 1 != 0);
927        let r = self.sr() >> 1;
928        self.set_dr(r);
929        self.set_sz_from(r);
930        self.reset_prefix();
931    }
932
933    // $04 rol
934    fn i_rol(&mut self) {
935        let carry = self.sr() & 0x8000 != 0;
936        let r = (self.sr() << 1) | u16::from(self.flag(SFR_CY));
937        self.set_dr(r);
938        self.set_flag(SFR_S, r & 0x8000 != 0);
939        self.set_flag(SFR_CY, carry);
940        self.set_flag(SFR_Z, r == 0);
941        self.reset_prefix();
942    }
943
944    // $05-$0f branch (delay slot via the pipeline)
945    fn i_branch(&mut self, take: bool, mem: &mut GsuMem) {
946        let displacement = self.pipe(mem) as i8;
947        if take {
948            self.r[15] = self.r[15].wrapping_add(displacement as u16);
949            self.r15_mod = true;
950        }
951    }
952
953    // $10-$1f to rN / move rN
954    fn i_to_move(&mut self, n: usize) {
955        if !self.flag(SFR_B) {
956            self.dreg = n;
957        } else {
958            let v = self.sr();
959            self.write_r(n, v);
960            self.reset_prefix();
961        }
962    }
963
964    // $20-$2f with rN
965    fn i_with(&mut self, n: usize) {
966        self.sreg = n;
967        self.dreg = n;
968        self.set_flag(SFR_B, true);
969    }
970
971    // $30-$3b stw (rN) / stb (rN)
972    fn i_store(&mut self, n: usize, mem: &mut GsuMem) {
973        self.ramaddr = self.r[n];
974        let s = self.sr();
975        self.write_ram_buffer(mem, self.ramaddr, s as u8);
976        if !self.alt1() {
977            self.write_ram_buffer(mem, self.ramaddr ^ 1, (s >> 8) as u8);
978        }
979        self.reset_prefix();
980    }
981
982    // $3c loop
983    fn i_loop(&mut self) {
984        self.r[12] = self.r[12].wrapping_sub(1);
985        self.set_flag(SFR_S, self.r[12] & 0x8000 != 0);
986        self.set_flag(SFR_Z, self.r[12] == 0);
987        if !self.flag(SFR_Z) {
988            self.r[15] = self.r[13];
989            self.r15_mod = true;
990        }
991        self.reset_prefix();
992    }
993
994    // $3d/$3e/$3f alt prefixes
995    fn i_alt1(&mut self) {
996        self.set_flag(SFR_B, false);
997        self.set_flag(SFR_ALT1, true);
998    }
999    fn i_alt2(&mut self) {
1000        self.set_flag(SFR_B, false);
1001        self.set_flag(SFR_ALT2, true);
1002    }
1003    fn i_alt3(&mut self) {
1004        self.set_flag(SFR_B, false);
1005        self.set_flag(SFR_ALT1, true);
1006        self.set_flag(SFR_ALT2, true);
1007    }
1008
1009    // $40-$4b ldw (rN) / ldb (rN)
1010    fn i_load(&mut self, n: usize, mem: &mut GsuMem) {
1011        self.ramaddr = self.r[n];
1012        let mut v = u16::from(self.read_ram_buffer(mem, self.ramaddr));
1013        if !self.alt1() {
1014            v |= u16::from(self.read_ram_buffer(mem, self.ramaddr ^ 1)) << 8;
1015        }
1016        self.set_dr(v);
1017        self.reset_prefix();
1018    }
1019
1020    // $4c plot / rpix
1021    fn i_plot_rpix(&mut self, mem: &mut GsuMem) {
1022        if !self.alt1() {
1023            self.plot(self.r[1] as u8, self.r[2] as u8, mem);
1024            self.r[1] = self.r[1].wrapping_add(1);
1025        } else {
1026            let v = u16::from(self.rpix(self.r[1] as u8, self.r[2] as u8, mem));
1027            self.set_dr(v);
1028            self.set_sz_from(v);
1029        }
1030        self.reset_prefix();
1031    }
1032
1033    /// Plot a pixel at `(x, y)` into the pixel cache (ares `SuperFX::plot`), evicting a completed
1034    /// strip to the Game Pak RAM via `mem`.
1035    fn plot(&mut self, x: u8, y: u8, mem: &mut GsuMem) {
1036        if !self.por_transparent {
1037            let transparent = if self.scmr_md == 3 {
1038                if self.por_freezehigh {
1039                    self.colr & 0x0f == 0
1040                } else {
1041                    self.colr == 0
1042                }
1043            } else {
1044                self.colr & 0x0f == 0
1045            };
1046            if transparent {
1047                return;
1048            }
1049        }
1050
1051        let mut color = self.colr;
1052        if self.por_dither && self.scmr_md != 3 {
1053            if (x ^ y) & 1 != 0 {
1054                color >>= 4;
1055            }
1056            color &= 0x0f;
1057        }
1058
1059        let offset = (u16::from(y) << 5) + (u16::from(x) >> 3);
1060        if offset != self.pixelcache[0].offset {
1061            let evicted = self.pixelcache[1];
1062            self.flush_pixel_cache_into(evicted, mem);
1063            self.pixelcache[1] = self.pixelcache[0];
1064            self.pixelcache[0].bitpend = 0;
1065            self.pixelcache[0].offset = offset;
1066        }
1067
1068        let xi = ((x & 7) ^ 7) as usize;
1069        self.pixelcache[0].data[xi] = color;
1070        self.pixelcache[0].bitpend |= 1 << xi;
1071        if self.pixelcache[0].bitpend == 0xff {
1072            let evicted = self.pixelcache[1];
1073            self.flush_pixel_cache_into(evicted, mem);
1074            self.pixelcache[1] = self.pixelcache[0];
1075            self.pixelcache[0].bitpend = 0;
1076        }
1077    }
1078
1079    // $4d swap
1080    fn i_swap(&mut self) {
1081        let r = (self.sr() >> 8) | (self.sr() << 8);
1082        self.set_dr(r);
1083        self.set_sz_from(r);
1084        self.reset_prefix();
1085    }
1086
1087    // $4e color / cmode
1088    fn i_color_cmode(&mut self) {
1089        if self.alt1() {
1090            self.set_por(self.sr() as u8);
1091        } else {
1092            self.colr = self.color(self.sr() as u8);
1093        }
1094        self.reset_prefix();
1095    }
1096
1097    /// Decode the POR (plot-option register) byte (ares `POR::operator=`).
1098    fn set_por(&mut self, data: u8) {
1099        self.por_obj = data & 0x10 != 0;
1100        self.por_freezehigh = data & 0x08 != 0;
1101        self.por_highnibble = data & 0x04 != 0;
1102        self.por_dither = data & 0x02 != 0;
1103        self.por_transparent = data & 0x01 != 0;
1104    }
1105
1106    // $4f not
1107    fn i_not(&mut self) {
1108        let r = !self.sr();
1109        self.set_dr(r);
1110        self.set_sz_from(r);
1111        self.reset_prefix();
1112    }
1113
1114    // $50-$5f add/adc rN/#N
1115    fn i_add_adc(&mut self, n: usize, _mem: &mut GsuMem) {
1116        let operand: i32 = if self.alt2() {
1117            n as i32
1118        } else {
1119            i32::from(self.r[n])
1120        };
1121        let sr = i32::from(self.sr());
1122        let carry = if self.alt1() {
1123            i32::from(self.flag(SFR_CY))
1124        } else {
1125            0
1126        };
1127        let r = sr + operand + carry;
1128        self.set_flag(SFR_OV, !(sr ^ operand) & (operand ^ r) & 0x8000 != 0);
1129        self.set_flag(SFR_S, r & 0x8000 != 0);
1130        self.set_flag(SFR_CY, r >= 0x10000);
1131        self.set_flag(SFR_Z, (r as u16) == 0);
1132        self.set_dr(r as u16);
1133        self.reset_prefix();
1134    }
1135
1136    // $60-$6f sub/sbc/cmp
1137    fn i_sub_sbc_cmp(&mut self, n: usize, _mem: &mut GsuMem) {
1138        let operand: i32 = if !self.alt2() || self.alt1() {
1139            i32::from(self.r[n])
1140        } else {
1141            n as i32
1142        };
1143        let sr = i32::from(self.sr());
1144        let borrow = if !self.alt2() && self.alt1() {
1145            i32::from(!self.flag(SFR_CY))
1146        } else {
1147            0
1148        };
1149        let r = sr - operand - borrow;
1150        self.set_flag(SFR_OV, (sr ^ operand) & (sr ^ r) & 0x8000 != 0);
1151        self.set_flag(SFR_S, r & 0x8000 != 0);
1152        self.set_flag(SFR_CY, r >= 0);
1153        self.set_flag(SFR_Z, (r as u16) == 0);
1154        if !self.alt2() || !self.alt1() {
1155            self.set_dr(r as u16);
1156        }
1157        self.reset_prefix();
1158    }
1159
1160    // $70 merge
1161    fn i_merge(&mut self) {
1162        let r = (self.r[7] & 0xff00) | (self.r[8] >> 8);
1163        self.set_dr(r);
1164        self.set_flag(SFR_OV, r & 0xc0c0 != 0);
1165        self.set_flag(SFR_S, r & 0x8080 != 0);
1166        self.set_flag(SFR_CY, r & 0xe0e0 != 0);
1167        self.set_flag(SFR_Z, r & 0xf0f0 != 0);
1168        self.reset_prefix();
1169    }
1170
1171    // $71-$7f and/bic
1172    fn i_and_bic(&mut self, n: usize, _mem: &mut GsuMem) {
1173        let operand = if self.alt2() { n as u16 } else { self.r[n] };
1174        let operand = if self.alt1() { !operand } else { operand };
1175        let r = self.sr() & operand;
1176        self.set_dr(r);
1177        self.set_sz_from(r);
1178        self.reset_prefix();
1179    }
1180
1181    // $80-$8f mult/umult
1182    fn i_mult_umult(&mut self, n: usize, mem: &mut GsuMem) {
1183        let operand = if self.alt2() { n as u16 } else { self.r[n] };
1184        let r = if self.alt1() {
1185            u16::from(self.sr() as u8).wrapping_mul(u16::from(operand as u8))
1186        } else {
1187            ((self.sr() as i8 as i16).wrapping_mul(operand as i8 as i16)) as u16
1188        };
1189        self.set_dr(r);
1190        self.set_sz_from(r);
1191        self.reset_prefix();
1192        if !self.cfgr_ms0 {
1193            self.step(if self.clsr { 1 } else { 2 }, mem);
1194        }
1195    }
1196
1197    // $90 sbk
1198    fn i_sbk(&mut self, mem: &mut GsuMem) {
1199        let s = self.sr();
1200        self.write_ram_buffer(mem, self.ramaddr, s as u8);
1201        self.write_ram_buffer(mem, self.ramaddr ^ 1, (s >> 8) as u8);
1202        self.reset_prefix();
1203    }
1204
1205    // $91-$94 link #N
1206    fn i_link(&mut self, n: usize) {
1207        self.r[11] = self.r[15].wrapping_add(n as u16);
1208        self.reset_prefix();
1209    }
1210
1211    // $95 sex
1212    fn i_sex(&mut self) {
1213        let r = (self.sr() as i8 as i16) as u16;
1214        self.set_dr(r);
1215        self.set_sz_from(r);
1216        self.reset_prefix();
1217    }
1218
1219    // $96 asr/div2
1220    fn i_asr_div2(&mut self) {
1221        // ares: dr = ((i16)sr >> 1) + (alt1 ? ((sr + 1) >> 16) : 0). The 32-bit `(sr + 1) >> 16`
1222        // term is the div2 round-toward-zero correction; it is 0 for any 16-bit `sr` except the
1223        // negative-odd boundary, so the faithful 32-bit form is kept verbatim.
1224        self.set_flag(SFR_CY, self.sr() & 1 != 0);
1225        let correction = if self.alt1() {
1226            (i32::from(self.sr()) + 1) >> 16
1227        } else {
1228            0
1229        };
1230        let r = ((i32::from(self.sr() as i16) >> 1) + correction) as u16;
1231        self.set_dr(r);
1232        self.set_sz_from(r);
1233        self.reset_prefix();
1234    }
1235
1236    // $97 ror
1237    fn i_ror(&mut self) {
1238        let carry = self.sr() & 1 != 0;
1239        let r = (u16::from(self.flag(SFR_CY)) << 15) | (self.sr() >> 1);
1240        self.set_dr(r);
1241        self.set_flag(SFR_S, r & 0x8000 != 0);
1242        self.set_flag(SFR_CY, carry);
1243        self.set_flag(SFR_Z, r == 0);
1244        self.reset_prefix();
1245    }
1246
1247    // $98-$9d jmp/ljmp
1248    fn i_jmp_ljmp(&mut self, n: usize) {
1249        if !self.alt1() {
1250            self.r[15] = self.r[n];
1251            self.r15_mod = true;
1252        } else {
1253            self.pbr = (self.r[n] & 0x7f) as u8;
1254            self.r[15] = self.sr();
1255            self.r15_mod = true;
1256            self.cbr = self.r[15] & 0xfff0;
1257            self.flush_cache();
1258        }
1259        self.reset_prefix();
1260    }
1261
1262    // $9e lob
1263    fn i_lob(&mut self) {
1264        let r = self.sr() & 0xff;
1265        self.set_dr(r);
1266        self.set_flag(SFR_S, r & 0x80 != 0);
1267        self.set_flag(SFR_Z, r == 0);
1268        self.reset_prefix();
1269    }
1270
1271    // $9f fmult/lmult
1272    fn i_fmult_lmult(&mut self, mem: &mut GsuMem) {
1273        let result = ((self.sr() as i16 as i32) * (self.r[6] as i16 as i32)) as u32;
1274        if self.alt1() {
1275            self.r[4] = result as u16;
1276        }
1277        let r = (result >> 16) as u16;
1278        self.set_dr(r);
1279        self.set_flag(SFR_S, r & 0x8000 != 0);
1280        self.set_flag(SFR_CY, result & 0x8000 != 0);
1281        self.set_flag(SFR_Z, r == 0);
1282        self.reset_prefix();
1283        let mul = if self.cfgr_ms0 { 3 } else { 7 };
1284        self.step(mul * if self.clsr { 1 } else { 2 }, mem);
1285    }
1286
1287    // $a0-$af ibt/lms/sms
1288    fn i_ibt_lms_sms(&mut self, n: usize, mem: &mut GsuMem) {
1289        if self.alt1() {
1290            self.ramaddr = u16::from(self.pipe(mem)) << 1;
1291            let lo = u16::from(self.read_ram_buffer(mem, self.ramaddr));
1292            let hi = u16::from(self.read_ram_buffer(mem, self.ramaddr ^ 1)) << 8;
1293            self.write_r(n, hi | lo);
1294        } else if self.alt2() {
1295            self.ramaddr = u16::from(self.pipe(mem)) << 1;
1296            let v = self.r[n];
1297            self.write_ram_buffer(mem, self.ramaddr, v as u8);
1298            self.write_ram_buffer(mem, self.ramaddr ^ 1, (v >> 8) as u8);
1299        } else {
1300            let imm = (self.pipe(mem) as i8 as i16) as u16;
1301            self.write_r(n, imm);
1302        }
1303        self.reset_prefix();
1304    }
1305
1306    // $b0-$bf from/moves
1307    fn i_from_moves(&mut self, n: usize) {
1308        if !self.flag(SFR_B) {
1309            self.sreg = n;
1310        } else {
1311            let v = self.r[n];
1312            self.set_dr(v);
1313            self.set_flag(SFR_OV, v & 0x80 != 0);
1314            self.set_flag(SFR_S, v & 0x8000 != 0);
1315            self.set_flag(SFR_Z, v == 0);
1316            self.reset_prefix();
1317        }
1318    }
1319
1320    // $c0 hib
1321    fn i_hib(&mut self) {
1322        let r = self.sr() >> 8;
1323        self.set_dr(r);
1324        self.set_flag(SFR_S, r & 0x80 != 0);
1325        self.set_flag(SFR_Z, r == 0);
1326        self.reset_prefix();
1327    }
1328
1329    // $c1-$cf or/xor
1330    fn i_or_xor(&mut self, n: usize, _mem: &mut GsuMem) {
1331        let operand = if self.alt2() { n as u16 } else { self.r[n] };
1332        let r = if self.alt1() {
1333            self.sr() ^ operand
1334        } else {
1335            self.sr() | operand
1336        };
1337        self.set_dr(r);
1338        self.set_sz_from(r);
1339        self.reset_prefix();
1340    }
1341
1342    // $d0-$de inc
1343    fn i_inc(&mut self, n: usize) {
1344        let v = self.r[n].wrapping_add(1);
1345        self.write_r(n, v);
1346        self.set_sz_from(v);
1347        self.reset_prefix();
1348    }
1349
1350    // $df getc/ramb/romb
1351    fn i_getc_ramb_romb(&mut self, mem: &mut GsuMem) {
1352        if !self.alt2() {
1353            let c = self.read_rom_buffer(mem);
1354            self.colr = self.color(c);
1355        } else if !self.alt1() {
1356            self.sync_ram_buffer(mem);
1357            self.rambr = self.sr() & 0x01 != 0;
1358        } else {
1359            self.sync_rom_buffer(mem);
1360            self.rombr = (self.sr() & 0x7f) as u8;
1361        }
1362        self.reset_prefix();
1363    }
1364
1365    // $e0-$ee dec
1366    fn i_dec(&mut self, n: usize) {
1367        let v = self.r[n].wrapping_sub(1);
1368        self.write_r(n, v);
1369        self.set_sz_from(v);
1370        self.reset_prefix();
1371    }
1372
1373    // $ef getb/getbh/getbl/getbs
1374    fn i_getb(&mut self, mem: &mut GsuMem) {
1375        let rb = u16::from(self.read_rom_buffer(mem));
1376        let mode = (u8::from(self.alt2()) << 1) | u8::from(self.alt1());
1377        let r = match mode {
1378            0 => rb,
1379            1 => (rb << 8) | (self.sr() & 0xff),
1380            2 => (self.sr() & 0xff00) | rb,
1381            _ => (rb as u8 as i8 as i16) as u16,
1382        };
1383        self.set_dr(r);
1384        self.reset_prefix();
1385    }
1386
1387    // $f0-$ff iwt/lm/sm
1388    fn i_iwt_lm_sm(&mut self, n: usize, mem: &mut GsuMem) {
1389        if self.alt1() {
1390            let mut addr = u16::from(self.pipe(mem));
1391            addr |= u16::from(self.pipe(mem)) << 8;
1392            self.ramaddr = addr;
1393            let lo = u16::from(self.read_ram_buffer(mem, addr));
1394            let hi = u16::from(self.read_ram_buffer(mem, addr ^ 1)) << 8;
1395            self.write_r(n, hi | lo);
1396        } else if self.alt2() {
1397            let mut addr = u16::from(self.pipe(mem));
1398            addr |= u16::from(self.pipe(mem)) << 8;
1399            self.ramaddr = addr;
1400            let v = self.r[n];
1401            self.write_ram_buffer(mem, addr, v as u8);
1402            self.write_ram_buffer(mem, addr ^ 1, (v >> 8) as u8);
1403        } else {
1404            let lo = u16::from(self.pipe(mem));
1405            let hi = u16::from(self.pipe(mem)) << 8;
1406            self.write_r(n, hi | lo);
1407        }
1408        self.reset_prefix();
1409    }
1410
1411    /// Bound on `pending_clocks`' saved length: `step` pushes at most a handful of bus-access
1412    /// checkpoints per instruction, so a claimed length beyond this is corrupt/hostile input, not
1413    /// a value real execution could ever produce (the zip-bomb-style "reject an absurd claimed
1414    /// size" posture, not a hardware width mask).
1415    const MAX_SAVED_PENDING_CLOCKS: usize = 64;
1416
1417    /// Write this core's full mutable state — every register, the status/control fields, both
1418    /// bus-buffer latches, the opcode cache, the plot pixel cache, the liveness counters, and the
1419    /// in-flight per-access checkpoint queue (`pending_clocks`/`pending_idx`/`owed`) — into a
1420    /// `"GSU0"` section. There is no firmware/ROM byte here to exclude: the GSU's program lives in
1421    /// the cart's own ROM, which `System::save_state` captures separately (`docs/adr/0003`). The
1422    /// checkpoint queue matters because [`Self::tick`]-driven (master-clock-interleaved) execution
1423    /// can leave a `Go` burst genuinely mid-flight at any save point, unlike the run-to-completion
1424    /// [`Self::run_until_stopped`] path, which always drains it back to empty first.
1425    pub fn save_state(&self, w: &mut SaveWriter) {
1426        w.section(*b"GSU0", |s| {
1427            for &reg in &self.r {
1428                s.write_u16(reg);
1429            }
1430            s.write_bool(self.r14_mod);
1431            s.write_bool(self.r15_mod);
1432            s.write_u16(self.sfr);
1433            #[allow(clippy::cast_possible_truncation)] // sreg/dreg are always 0-15
1434            s.write_u8(self.sreg as u8);
1435            #[allow(clippy::cast_possible_truncation)]
1436            s.write_u8(self.dreg as u8);
1437            s.write_u8(self.pipeline);
1438            s.write_u8(self.pbr);
1439            s.write_u8(self.rombr);
1440            s.write_bool(self.rambr);
1441            s.write_u16(self.cbr);
1442            s.write_u8(self.scbr);
1443            s.write_u8(self.scmr_ht);
1444            s.write_bool(self.scmr_ron);
1445            s.write_bool(self.scmr_ran);
1446            s.write_u8(self.scmr_md);
1447            s.write_u8(self.colr);
1448            s.write_bool(self.por_obj);
1449            s.write_bool(self.por_freezehigh);
1450            s.write_bool(self.por_highnibble);
1451            s.write_bool(self.por_dither);
1452            s.write_bool(self.por_transparent);
1453            s.write_bool(self.bramr);
1454            s.write_u8(self.vcr);
1455            s.write_bool(self.cfgr_irq);
1456            s.write_bool(self.cfgr_ms0);
1457            s.write_bool(self.clsr);
1458            s.write_u32(self.romcl);
1459            s.write_u8(self.romdr);
1460            s.write_u32(self.ramcl);
1461            s.write_u16(self.ramar);
1462            s.write_u8(self.ramdr);
1463            s.write_u16(self.ramaddr);
1464            s.write_bytes(&*self.cache_buffer);
1465            for &valid in &self.cache_valid {
1466                s.write_bool(valid);
1467            }
1468            for pc in &self.pixelcache {
1469                s.write_u16(pc.offset);
1470                s.write_u8(pc.bitpend);
1471                s.write_bytes(&pc.data);
1472            }
1473            s.write_u64(self.clocks);
1474            s.write_u64(self.instructions);
1475            #[allow(clippy::cast_possible_truncation)] // bounded by MAX_SAVED_PENDING_CLOCKS
1476            s.write_u32(self.pending_clocks.len() as u32);
1477            for &c in &self.pending_clocks {
1478                s.write_u32(c);
1479            }
1480            #[allow(clippy::cast_possible_truncation)] // <= pending_clocks.len(), same bound
1481            s.write_u32(self.pending_idx as u32);
1482            s.write_u32(self.owed);
1483        });
1484    }
1485
1486    /// The inverse of [`Self::save_state`].
1487    ///
1488    /// # Errors
1489    /// [`SaveStateError`] on truncated/corrupt input, a section with unconsumed trailing bytes, or
1490    /// [`SaveStateError::Invalid`] if the saved `pending_clocks` length exceeds
1491    /// `MAX_SAVED_PENDING_CLOCKS` or `pending_idx` exceeds the restored queue's length
1492    /// (both would otherwise let a corrupted save-state desync [`Self::step_one`]'s cursor).
1493    /// `sreg`/`dreg` are masked to 4 bits — they index the 16-entry register file directly.
1494    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
1495        let mut s = r.expect_section(*b"GSU0")?;
1496        for reg in &mut self.r {
1497            *reg = s.read_u16()?;
1498        }
1499        self.r14_mod = s.read_bool()?;
1500        self.r15_mod = s.read_bool()?;
1501        self.sfr = s.read_u16()?;
1502        self.sreg = usize::from(s.read_u8()? & 0xF);
1503        self.dreg = usize::from(s.read_u8()? & 0xF);
1504        self.pipeline = s.read_u8()?;
1505        // pbr/rombr/cbr/scmr_ht/scmr_md are masked identically on every normal write (see the
1506        // cited write sites); masking here too keeps a restored register within the same range
1507        // real execution could ever produce.
1508        self.pbr = s.read_u8()? & 0x7F;
1509        self.rombr = s.read_u8()? & 0x7F;
1510        self.rambr = s.read_bool()?;
1511        self.cbr = s.read_u16()? & 0xFFF0;
1512        self.scbr = s.read_u8()?;
1513        self.scmr_ht = s.read_u8()? & 0x03;
1514        self.scmr_ron = s.read_bool()?;
1515        self.scmr_ran = s.read_bool()?;
1516        self.scmr_md = s.read_u8()? & 0x03;
1517        self.colr = s.read_u8()?;
1518        self.por_obj = s.read_bool()?;
1519        self.por_freezehigh = s.read_bool()?;
1520        self.por_highnibble = s.read_bool()?;
1521        self.por_dither = s.read_bool()?;
1522        self.por_transparent = s.read_bool()?;
1523        self.bramr = s.read_bool()?;
1524        self.vcr = s.read_u8()?;
1525        self.cfgr_irq = s.read_bool()?;
1526        self.cfgr_ms0 = s.read_bool()?;
1527        self.clsr = s.read_bool()?;
1528        self.romcl = s.read_u32()?;
1529        self.romdr = s.read_u8()?;
1530        self.ramcl = s.read_u32()?;
1531        self.ramar = s.read_u16()?;
1532        self.ramdr = s.read_u8()?;
1533        self.ramaddr = s.read_u16()?;
1534        self.cache_buffer.copy_from_slice(s.read_bytes(512)?);
1535        for valid in &mut self.cache_valid {
1536            *valid = s.read_bool()?;
1537        }
1538        for pc in &mut self.pixelcache {
1539            pc.offset = s.read_u16()?;
1540            pc.bitpend = s.read_u8()?;
1541            pc.data.copy_from_slice(s.read_bytes(8)?);
1542        }
1543        self.clocks = s.read_u64()?;
1544        self.instructions = s.read_u64()?;
1545        let pending_len = s.read_u32()? as usize;
1546        if pending_len > Self::MAX_SAVED_PENDING_CLOCKS {
1547            return Err(SaveStateError::Invalid(alloc::format!(
1548                "GSU pending_clocks length {pending_len} exceeds the sane bound of {}",
1549                Self::MAX_SAVED_PENDING_CLOCKS
1550            )));
1551        }
1552        self.pending_clocks.clear();
1553        for _ in 0..pending_len {
1554            self.pending_clocks.push(s.read_u32()?);
1555        }
1556        let pending_idx = s.read_u32()? as usize;
1557        if pending_idx > self.pending_clocks.len() {
1558            return Err(SaveStateError::Invalid(alloc::format!(
1559                "GSU pending_idx {pending_idx} exceeds the restored queue length {}",
1560                self.pending_clocks.len()
1561            )));
1562        }
1563        self.pending_idx = pending_idx;
1564        self.owed = s.read_u32()?;
1565        if s.remaining() != 0 {
1566            return Err(SaveStateError::Invalid(alloc::format!(
1567                "GSU0 section has {} trailing byte(s)",
1568                s.remaining()
1569            )));
1570        }
1571        Ok(())
1572    }
1573}
1574
1575#[cfg(test)]
1576mod tests {
1577    use super::*;
1578
1579    /// Replay `load_state`'s own field order up to (not including) `pending_clocks`' length
1580    /// prefix, discarding each value, and return the byte offset of that length prefix within
1581    /// `bytes` — computed dynamically (not hardcoded) so it can't silently go stale if a field is
1582    /// added/removed/reordered above it.
1583    fn pending_len_offset(bytes: &[u8]) -> usize {
1584        let mut r = SaveReader::new(bytes);
1585        let mut s = r.expect_section(*b"GSU0").unwrap();
1586        for _ in 0..16 {
1587            s.read_u16().unwrap(); // r[16]
1588        }
1589        s.read_bool().unwrap(); // r14_mod
1590        s.read_bool().unwrap(); // r15_mod
1591        s.read_u16().unwrap(); // sfr
1592        s.read_u8().unwrap(); // sreg
1593        s.read_u8().unwrap(); // dreg
1594        s.read_u8().unwrap(); // pipeline
1595        s.read_u8().unwrap(); // pbr
1596        s.read_u8().unwrap(); // rombr
1597        s.read_bool().unwrap(); // rambr
1598        s.read_u16().unwrap(); // cbr
1599        s.read_u8().unwrap(); // scbr
1600        s.read_u8().unwrap(); // scmr_ht
1601        s.read_bool().unwrap(); // scmr_ron
1602        s.read_bool().unwrap(); // scmr_ran
1603        s.read_u8().unwrap(); // scmr_md
1604        s.read_u8().unwrap(); // colr
1605        for _ in 0..5 {
1606            s.read_bool().unwrap(); // por_*
1607        }
1608        s.read_bool().unwrap(); // bramr
1609        s.read_u8().unwrap(); // vcr
1610        s.read_bool().unwrap(); // cfgr_irq
1611        s.read_bool().unwrap(); // cfgr_ms0
1612        s.read_bool().unwrap(); // clsr
1613        s.read_u32().unwrap(); // romcl
1614        s.read_u8().unwrap(); // romdr
1615        s.read_u32().unwrap(); // ramcl
1616        s.read_u16().unwrap(); // ramar
1617        s.read_u8().unwrap(); // ramdr
1618        s.read_u16().unwrap(); // ramaddr
1619        s.read_bytes(512).unwrap(); // cache_buffer
1620        for _ in 0..32 {
1621            s.read_bool().unwrap(); // cache_valid
1622        }
1623        for _ in 0..2 {
1624            s.read_u16().unwrap(); // pixelcache offset
1625            s.read_u8().unwrap(); // pixelcache bitpend
1626            s.read_bytes(8).unwrap(); // pixelcache data
1627        }
1628        s.read_u64().unwrap(); // clocks
1629        s.read_u64().unwrap(); // instructions
1630        bytes.len() - s.remaining()
1631    }
1632
1633    #[test]
1634    fn oversized_pending_clocks_length_is_rejected_not_trusted() {
1635        let gsu = Gsu::new();
1636        let mut w = SaveWriter::new();
1637        gsu.save_state(&mut w);
1638        let mut bytes = w.into_bytes();
1639
1640        let offset = pending_len_offset(&bytes);
1641        // No real execution can ever produce a pending_clocks length this large (step() only
1642        // ever pushes a handful of checkpoints per instruction) — a hand-edited/corrupted
1643        // save-state claiming otherwise must be rejected before it's ever trusted to read that
1644        // many entries.
1645        bytes[offset..offset + 4].copy_from_slice(&1000u32.to_le_bytes());
1646
1647        let mut fresh = Gsu::new();
1648        let mut r = SaveReader::new(&bytes);
1649        assert!(matches!(
1650            fresh.load_state(&mut r),
1651            Err(SaveStateError::Invalid(_))
1652        ));
1653    }
1654
1655    #[test]
1656    fn pending_idx_beyond_the_restored_queue_is_rejected_not_trusted() {
1657        let gsu = Gsu::new();
1658        let mut w = SaveWriter::new();
1659        gsu.save_state(&mut w);
1660        let mut bytes = w.into_bytes();
1661
1662        // A fresh Gsu's pending_clocks is empty (length 0), so pending_idx immediately follows
1663        // the 4-byte length field with zero entries in between.
1664        let len_offset = pending_len_offset(&bytes);
1665        let idx_offset = len_offset + 4;
1666        bytes[idx_offset..idx_offset + 4].copy_from_slice(&1u32.to_le_bytes());
1667
1668        let mut fresh = Gsu::new();
1669        let mut r = SaveReader::new(&bytes);
1670        assert!(matches!(
1671            fresh.load_state(&mut r),
1672            Err(SaveStateError::Invalid(_))
1673        ));
1674    }
1675}