Skip to main content

rustynes_core/
input_device.rs

1//! Optional non-standard input-device overlays for the `$4016`/`$4017`
2//! controller ports: the Arkanoid "Vaus" paddle and the NES Zapper light gun.
3//!
4//! These are **opt-in overlays**. The bus holds an `Option<InputDevice>` per
5//! port; when a port has no overlay device (the default) the standard
6//! controller / Four Score serial path runs completely unchanged, so the
7//! default + Four Score reads stay byte-identical and the determinism
8//! contract is preserved. A device is only consulted when explicitly attached
9//! via [`crate::Nes::set_paddle`] / [`crate::Nes::set_zapper`].
10//!
11//! ## Vaus paddle (Arkanoid controller)
12//!
13//! Per the `NESdev` "Arkanoid controller" page (NES 7-pin version), the device
14//! reports on the player-2 port (`$4017`):
15//!
16//! ```text
17//! 7  bit  0
18//! ---- ----
19//! xxxD Bxxx
20//!    | |
21//!    | +---- Fire button (1: pressed)        -> bit 3
22//!    +------ Serial control knob data        -> bit 4
23//!            (8/9-bit, inverted, MSb first)
24//! ```
25//!
26//! A write of `$4016` bit 0 = 1 -> 0 (the standard controller strobe) starts a
27//! "conversion": the 8-bit potentiometer value is latched MSb-first into the
28//! shift register. Each `$4017` read shifts out the next bit (on bit 4),
29//! **inverted** on the wire. After the register empties, reads repeat the
30//! serial-in bit (the 9th / `LSb`). The fire button (bit 3) is returned directly
31//! and is unaffected by the strobe.
32//!
33//! The in-tree `vaus-test` ROM (Damian Yerrick) documents the NES wiring as
34//! `$4017 D3: Button`, `$4017 D4: Position (8 bits, MSB first)` — matching the
35//! wiki layout above.
36//!
37//! ## Zapper light gun
38//!
39//! Per the `NESdev` "Zapper" page (NES variant), the device reports on its port:
40//!
41//! ```text
42//! 7  bit  0
43//! ---- ----
44//! xxxT Wxxx
45//!    | |
46//!    | +---- Light sensed (0: detected; 1: NOT detected)  -> bit 3
47//!    +------ Trigger (1: pulled/half-pulled; 0: released)  -> bit 4
48//! ```
49//!
50//! Note the inverted light polarity: bit 3 is **0** while light is detected and
51//! **1** otherwise. The light sensor stays active for roughly 19-26 scanlines
52//! after seeing a bright pixel (the photodiode capacitor drains exponentially);
53//! we use a simpler frame-granular model: a luminance threshold sampled at the
54//! aim point once per completed frame (sufficient because games re-sample every
55//! frame). The Zapper has no shift register — its byte is read in parallel and
56//! is independent of the strobe.
57
58/// The Arkanoid "Vaus" paddle overlay state.
59///
60/// Models the NES 7-pin variant on `$4017`: an 8-bit potentiometer value
61/// shifted out MSb-first (inverted on the wire) on bit 4, plus a fire button
62/// on bit 3.
63#[derive(Clone, Copy, Debug)]
64pub struct VausState {
65    /// The raw (pre-inversion) 8-bit potentiometer position. `$00` is the far
66    /// left, `$FF` the far right (per the wiki, turning right increases the
67    /// value).
68    pub(crate) position: u8,
69    /// Whether the fire button is currently held.
70    pub(crate) fire: bool,
71    /// 8-bit shift register, MSb-first readout. Reloaded from `position` on the
72    /// strobe falling edge (conversion latch). The serial-in bit (repeated
73    /// after the register empties) is the current `LSb`.
74    pub(crate) shift: u8,
75    /// Last strobe level written (bit 0 of `$4016`).
76    pub(crate) strobe: bool,
77}
78
79impl Default for VausState {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl VausState {
86    /// New paddle centered, button released.
87    #[must_use]
88    pub const fn new() -> Self {
89        Self {
90            position: 0x80,
91            fire: false,
92            shift: 0x80,
93            strobe: false,
94        }
95    }
96
97    /// Update the live paddle position + fire state. Takes effect on the next
98    /// conversion (strobe falling edge), matching the standard controller's
99    /// latch-on-strobe semantics.
100    pub const fn set(&mut self, position: u8, fire: bool) {
101        self.position = position;
102        self.fire = fire;
103        if self.strobe {
104            self.shift = position;
105        }
106    }
107
108    /// Handle a `$4016` strobe write. On the rising edge the conversion latches
109    /// the current position into the shift register (we model the conversion as
110    /// instantaneous, which is the standard fixed-position emulation choice).
111    pub const fn write_strobe(&mut self, value: u8) {
112        let new_strobe = value & 1 != 0;
113        if new_strobe {
114            self.shift = self.position;
115        }
116        self.strobe = new_strobe;
117    }
118
119    /// Read the device byte for a `$4017` access, advancing the shift register.
120    /// Returns the full 8-bit value already positioned on bits 3 (fire) and 4
121    /// (knob data); the caller ORs in the open-bus upper bits.
122    ///
123    /// Bit 4 carries the **inverted** `MSb` of the shift register (knob data is
124    /// inverted on the wire per the wiki). Bit 3 carries the fire button (1 =
125    /// pressed). All other bits are 0.
126    pub const fn read(&mut self) -> u8 {
127        // Knob data bit: MSb of the shift register, inverted on the wire.
128        let data_bit = (self.shift >> 7) & 1;
129        let wire_data = data_bit ^ 1;
130        // Shift left, feeding the LSb back into the serial-in position so that
131        // post-empty reads repeat the 9th (serial-in) bit per the wiki.
132        let serial_in = self.shift & 1;
133        self.shift = (self.shift << 1) | serial_in;
134        let fire = self.fire as u8;
135        (wire_data << 4) | (fire << 3)
136    }
137
138    /// Side-effect-free sample of the next device byte (debugger peek).
139    #[must_use]
140    pub const fn peek(&self) -> u8 {
141        let data_bit = (self.shift >> 7) & 1;
142        let wire_data = data_bit ^ 1;
143        let fire = self.fire as u8;
144        (wire_data << 4) | (fire << 3)
145    }
146
147    /// Reconstruct from save-state parts.
148    #[must_use]
149    pub const fn from_parts(position: u8, fire: bool, shift: u8, strobe: bool) -> Self {
150        Self {
151            position,
152            fire,
153            shift,
154            strobe,
155        }
156    }
157
158    /// Raw potentiometer position (save-state).
159    #[must_use]
160    pub const fn position_raw(&self) -> u8 {
161        self.position
162    }
163    /// Raw fire state (save-state).
164    #[must_use]
165    pub const fn fire_raw(&self) -> bool {
166        self.fire
167    }
168    /// Raw shift register (save-state).
169    #[must_use]
170    pub const fn shift_raw(&self) -> u8 {
171        self.shift
172    }
173    /// Raw strobe state (save-state).
174    #[must_use]
175    pub const fn strobe_raw(&self) -> bool {
176        self.strobe
177    }
178}
179
180/// The NES Zapper light-gun overlay state.
181///
182/// Models the NES variant: bit 3 = light sensed (0 detected / 1 not), bit 4 =
183/// trigger (1 pulled). Light detection samples the PPU framebuffer luminance at
184/// the aim point once per frame (a frame-granular model — see `light_seen`).
185#[derive(Clone, Copy, Debug, Default)]
186pub struct ZapperState {
187    /// Aim point X (0..256), screen pixel. Out-of-range = aimed off-screen.
188    pub(crate) x: u16,
189    /// Aim point Y (0..240), screen scanline. Out-of-range = aimed off-screen.
190    pub(crate) y: u16,
191    /// Whether the trigger is currently pulled.
192    pub(crate) trigger: bool,
193    /// Whether the photodiode currently sees light. Set by [`Self::sample_light`]
194    /// each frame from the framebuffer luminance at the aim point; while `true`,
195    /// bit 3 reads 0 (light detected), else bit 3 reads 1 (no light). This is a
196    /// frame-granular model: games re-sample every frame, so per-frame
197    /// resolution is sufficient (the wiki's ~19-26-scanline photodiode hold
198    /// matters only for sub-frame timing tricks, which the supported games do
199    /// not require).
200    pub(crate) light_seen: bool,
201}
202
203/// Luminance threshold (0..255, Rec.601-ish) above which a sampled framebuffer
204/// pixel counts as "bright enough" to trigger the photodiode.
205pub(crate) const ZAPPER_LUMA_THRESHOLD: u16 = 0x80;
206
207/// Photodiode **aperture radius** in pixels (v2.2.0 "Capstone" light-timing
208/// hardening). The real Zapper's lens focuses light from a small solid angle
209/// onto the photodiode, so the sensor integrates a *region* of the CRT phosphor,
210/// not a single dot. Sampling a `(2r+1) x (2r+1)` window around the aim point
211/// (rather than one pixel) hardens detection against sub-pixel aim error and
212/// single-pixel dropouts in the PPU output — matching how the hardware responds
213/// to the bright target the game flashes. Radius 1 = a 3x3 aperture.
214pub(crate) const ZAPPER_APERTURE_RADIUS: i32 = 1;
215
216/// Minimum number of bright pixels within the aperture required to assert
217/// "light detected". Requiring more than one rejects a lone stray-bright pixel
218/// (PPU edge artefact) as a false positive while still firing on the target
219/// flash, which lights the whole aperture. Calibrated for the 3x3 aperture.
220pub(crate) const ZAPPER_APERTURE_MIN_BRIGHT: u32 = 2;
221
222/// Photodiode hold, in scanlines (A3, v2.2.3 — used only by the opt-in
223/// temporal model).
224///
225/// The Zapper's photodiode charges when the CRT beam paints a bright pixel in
226/// its field of view, then drains exponentially: `NESdev` "Zapper" puts the
227/// resulting light-sense window at roughly **19-26 scanlines**. 22 is the
228/// midpoint, and the span is wide enough that no supported title distinguishes
229/// values inside it.
230pub(crate) const ZAPPER_LIGHT_HOLD_SCANLINES: u16 = 22;
231
232impl ZapperState {
233    /// New zapper aimed off-screen, trigger released, no light.
234    #[must_use]
235    pub const fn new() -> Self {
236        Self {
237            x: u16::MAX,
238            y: u16::MAX,
239            trigger: false,
240            light_seen: false,
241        }
242    }
243
244    /// Update the live aim point + trigger state.
245    pub const fn set(&mut self, x: u16, y: u16, trigger: bool) {
246        self.x = x;
247        self.y = y;
248        self.trigger = trigger;
249    }
250
251    /// Sample the framebuffer luminance over the photodiode **aperture** around
252    /// the aim point, setting `light_seen` when enough of the aperture is bright.
253    /// `framebuffer` is the PPU's RGBA8 256x240 buffer. Called once per frame by
254    /// the bus after the frame completes.
255    ///
256    /// v2.2.0 "Capstone" light-timing hardening: rather than sampling a single
257    /// pixel, the sensor integrates a `(2r+1) x (2r+1)` aperture
258    /// (`ZAPPER_APERTURE_RADIUS`) and asserts light only when at least
259    /// `ZAPPER_APERTURE_MIN_BRIGHT` pixels cross `ZAPPER_LUMA_THRESHOLD`. This
260    /// models the lens/photodiode field-of-view against the PPU's per-dot output:
261    /// the bright target the game flashes lights the whole aperture (robust
262    /// detection), while a black "blanked" background frame — or a lone stray
263    /// bright pixel — yields no light (no false positive). The computation is a
264    /// pure, deterministic function of the framebuffer + aim point, so it needs
265    /// no additional save-state and preserves the determinism contract.
266    ///
267    /// The temporal light-sense window (the ~19-26-scanline photodiode hold) is
268    /// finer than the per-frame sample resolution used here; the supported
269    /// light-gun titles re-poll every frame, so frame-granular sampling of the
270    /// presented framebuffer is sufficient. A full per-dot temporal integration
271    /// against the beam position is a documented future refinement — see
272    /// `docs/frontend.md`.
273    pub fn sample_light(&mut self, framebuffer: &[u8]) {
274        self.light_seen = Self::aperture_is_bright(framebuffer, self.x, self.y);
275    }
276
277    /// Shared aperture test: is the `(2r+1)x(2r+1)` photodiode field of view at
278    /// `(x, y)` bright enough to charge the sensor?
279    ///
280    /// A3 (v2.2.3) factored this out of [`Self::sample_light`] so the
281    /// frame-granular and beam-relative models cannot drift apart — the
282    /// temporal model differs from the frame model ONLY in *when* it samples,
283    /// never in what counts as light.
284    fn aperture_is_bright(framebuffer: &[u8], x: u16, y: u16) -> bool {
285        Self::aperture_is_bright_painted(framebuffer, x, y, None)
286    }
287
288    /// [`Self::aperture_is_bright`], restricted to rows the CRT beam has
289    /// **finished painting this frame**.
290    ///
291    /// `painted_before` is the scanline the beam is currently on: rows at or
292    /// after it are excluded, because the current row is only part-way drawn and
293    /// later rows still hold the PREVIOUS frame's pixels. `None` means the whole
294    /// framebuffer is current, which is true only for the end-of-frame sampler.
295    ///
296    /// # Why this is load-bearing
297    ///
298    /// A photodiode can only respond to light the phosphor has already emitted.
299    /// Without this clip the beam-relative model samples stale rows and reports
300    /// light on a screen that is entirely black — measured directly on *Duck
301    /// Hunt*'s light-test frame, where at scanline 96 the beam was 5 dots into
302    /// row 96 and the sampler read the previous frame's bright sky (`aim_luma
303    /// 152` on a frame whose mean luma is 0), then did it again at scanline 97
304    /// via the still-unpainted row 97 of the 3x3 aperture.
305    ///
306    /// That matters because *Duck Hunt* requires the gun to see **nothing** for
307    /// one frame before it will accept a shot, so a false positive here discards
308    /// every shot: the gun fires and nothing can ever be hit (v2.3.6).
309    fn aperture_is_bright_painted(
310        framebuffer: &[u8],
311        x: u16,
312        y: u16,
313        painted_before: Option<u16>,
314    ) -> bool {
315        const W: i32 = 256;
316        const H: i32 = 240;
317        let (ax, ay) = (i32::from(x), i32::from(y));
318        if ax >= W || ay >= H {
319            return false; // aimed off-screen: never sees light
320        }
321        // Rows `>= painted_before` are not yet emitted this frame. `None` (the
322        // end-of-frame sampler) admits the whole screen.
323        let row_limit = painted_before.map_or(H, i32::from);
324        let mut bright = 0u32;
325        let r = ZAPPER_APERTURE_RADIUS;
326        for dy in -r..=r {
327            for dx in -r..=r {
328                let (px, py) = (ax + dx, ay + dy);
329                if !(0..W).contains(&px) || !(0..H).contains(&py) {
330                    continue; // aperture clipped by the screen edge
331                }
332                if py >= row_limit {
333                    continue; // the beam has not finished this row this frame
334                }
335                // px/py are now bounded to the screen, so the linear index is
336                // non-negative and fits a usize.
337                let Ok(idx) = usize::try_from((py * W + px) * 4) else {
338                    continue;
339                };
340                if idx + 2 >= framebuffer.len() {
341                    continue; // guard against a partial framebuffer
342                }
343                let cr = u16::from(framebuffer[idx]);
344                let cg = u16::from(framebuffer[idx + 1]);
345                let cb = u16::from(framebuffer[idx + 2]);
346                // Rec.601 luma approximation (integer): (77*R + 150*G + 29*B) >> 8.
347                let luma = (77 * cr + 150 * cg + 29 * cb) >> 8;
348                if luma >= ZAPPER_LUMA_THRESHOLD {
349                    bright += 1;
350                }
351            }
352        }
353        bright >= ZAPPER_APERTURE_MIN_BRIGHT
354    }
355
356    /// A3 (v2.2.3): does the photodiode see light **right now**, given where
357    /// the CRT beam currently is?
358    ///
359    /// The frame-granular [`Self::sample_light`] answers "was the aim point
360    /// bright in the completed frame", which is constant for the whole frame —
361    /// so a game polling immediately after its flash and one polling 100
362    /// scanlines later get the same answer. Real hardware does not work that
363    /// way: the photodiode charges as the beam *passes* the aim point and
364    /// drains over ~19-26 scanlines afterwards.
365    ///
366    /// This models that directly, as a pure function of
367    /// `(framebuffer, aim, current scanline)`:
368    ///
369    /// * before the beam reaches the aim row (`scanline < y`) — dark, because
370    ///   this frame has not painted it yet;
371    /// * from the aim row until the hold expires — bright iff the aperture is
372    ///   bright **over the rows the beam has already finished**, per
373    ///   `aperture_is_bright_painted` (a plain code span, not an intra-doc link:
374    ///   that item is private and `rustdoc::private_intra_doc_links` is denied);
375    /// * after the hold — dark again, the capacitor having drained.
376    ///
377    /// Holding **no extra state** is deliberate: light is derived on demand at
378    /// read time rather than latched by a per-scanline callback, so it adds no
379    /// field to serialize, cannot desync a save state or a netplay rollback,
380    /// and keeps the determinism contract (same framebuffer + aim + scanline
381    /// always yields the same answer).
382    ///
383    /// # A wrong claim this used to make (v2.3.6)
384    ///
385    /// This doc previously ended: *"One consequence is physically right rather
386    /// than a compromise: the aperture rows below the beam still hold the
387    /// previous frame's pixels, which is exactly what the sensor sees, since the
388    /// beam has not repainted them yet."*
389    ///
390    /// **That is backwards.** A photodiode responds to light the phosphor has
391    /// *emitted*; a row the beam has not reached this frame is emitting nothing,
392    /// and its stale framebuffer contents are an artefact of how the emulator
393    /// stores pixels, not something a sensor could see. Reading those rows made
394    /// the model report light on an all-black screen — measured at scanline 96,
395    /// where the beam was 5 dots into row 96 and the sampler returned the
396    /// previous frame's sky at luma 152 on a frame whose mean luma was 0.
397    ///
398    /// Because *Duck Hunt* requires the gun to see nothing for one frame before
399    /// it will accept a shot, that false positive discarded every shot: the gun
400    /// fired and no duck could ever be hit. The rows are now clipped, and the
401    /// paragraph is kept rather than deleted because the plausible-sounding
402    /// wrong reasoning is what made the defect look intentional.
403    #[must_use]
404    pub fn light_at_scanline(&self, framebuffer: &[u8], scanline: u16) -> bool {
405        let y = self.y;
406        if scanline < y {
407            return false; // beam has not painted the aim row yet this frame
408        }
409        if scanline - y >= ZAPPER_LIGHT_HOLD_SCANLINES {
410            return false; // photodiode has drained
411        }
412        // Only rows the beam has FINISHED this frame can have emitted light —
413        // see `aperture_is_bright_painted` for what goes wrong without this.
414        Self::aperture_is_bright_painted(framebuffer, self.x, y, Some(scanline))
415    }
416
417    /// The device byte as [`Self::read`] would return it, but using the
418    /// beam-relative light state from [`Self::light_at_scanline`].
419    #[must_use]
420    pub fn read_at_scanline(&self, framebuffer: &[u8], scanline: u16) -> u8 {
421        let light_not_detected = u8::from(!self.light_at_scanline(framebuffer, scanline));
422        let trigger = u8::from(self.trigger);
423        (trigger << 4) | (light_not_detected << 3)
424    }
425
426    /// The device byte for a read taken **before the visible frame begins** —
427    /// the answer when the PPU's scanline is *negative*, which no light is
428    /// detectable for (the beam has painted nothing this frame yet).
429    ///
430    /// This is a total-conversion fallback, not a fix for a live defect. In this
431    /// engine `Ppu::scanline()` is non-negative on every region — the pre-render
432    /// line is 261 (NTSC) / 311 (PAL), not -1 — so the visible/vblank path
433    /// through [`Self::read_at_scanline`] already yields no-light for pre-render
434    /// (`prerender - y >= ZAPPER_LIGHT_HOLD_SCANLINES` for every on-screen aim),
435    /// and this branch is not reached. It exists so that the caller's
436    /// `u16::try_from(scanline)` has a *correct* `Err` answer — "no light yet" —
437    /// rather than the row-0 fold a bare `unwrap_or(0)` would produce, should a
438    /// future scanline convention (a -1 pre-render, as some emulators use) ever
439    /// hand this a negative value.
440    #[must_use]
441    pub const fn read_before_visible(&self) -> u8 {
442        let trigger = self.trigger as u8;
443        (trigger << 4) | (1 << 3) // light NOT detected (bit 3 is inverted)
444    }
445
446    /// The device byte for a `$4016`/`$4017` access. Bit 3 = light (0 detected /
447    /// 1 not), bit 4 = trigger (1 pulled). Independent of the strobe (the
448    /// Zapper has no shift register). The caller ORs in the open-bus upper bits.
449    #[must_use]
450    pub const fn read(&self) -> u8 {
451        let light_not_detected = (!self.light_seen) as u8;
452        let trigger = self.trigger as u8;
453        (trigger << 4) | (light_not_detected << 3)
454    }
455
456    /// Reconstruct from save-state parts.
457    #[must_use]
458    pub const fn from_parts(x: u16, y: u16, trigger: bool, light_seen: bool) -> Self {
459        Self {
460            x,
461            y,
462            trigger,
463            light_seen,
464        }
465    }
466
467    /// Raw aim X (save-state).
468    #[must_use]
469    pub const fn x_raw(&self) -> u16 {
470        self.x
471    }
472    /// Raw aim Y (save-state).
473    #[must_use]
474    pub const fn y_raw(&self) -> u16 {
475        self.y
476    }
477    /// Raw trigger state (save-state).
478    #[must_use]
479    pub const fn trigger_raw(&self) -> bool {
480        self.trigger
481    }
482    /// Raw light-seen state (save-state).
483    #[must_use]
484    pub const fn light_seen_raw(&self) -> bool {
485        self.light_seen
486    }
487}
488
489/// The NES Power Pad (a.k.a. Family Fun Fitness / Family Trainer mat) overlay.
490///
491/// A 12-button mat read on the player-2 port (`$4017`) through two 8-bit
492/// parallel-in/serial-out shift registers (a pair of 4021s), strobed by the
493/// standard `$4016` controller strobe. The 12 buttons are indexed 0..=11
494/// (matching the mat's "1".."12" labels); the frontend decides which physical
495/// keys map to which mat button (and any Side-A/Side-B row inversion).
496///
497/// Per the `NESdev` "Power Pad" page (and Mesen's implementation), the button
498/// bits load into two registers and shift out LSb-first on bits 3 and 4 of each
499/// `$4017` read:
500///
501/// - register L (bit 3 of the read): buttons 2, 1, 5, 9, 6, 10, 11, 7;
502/// - register H (bit 4 of the read): buttons 4, 3, 12, 8, then four `1` bits.
503///
504/// (Button numbers are 1-based here, matching the mat labels; the code uses
505/// 0-based indices.) Each read shifts both registers right and feeds `1`s in
506/// from the top, so post-shift reads settle to "no button".
507#[derive(Clone, Copy, Debug, Default)]
508pub struct PowerPadState {
509    /// Live pressed-button mask: bit `i` (0..=11) set = mat button `i+1` held.
510    pub(crate) buttons: u16,
511    /// Low shift register (read out on bit 3).
512    pub(crate) shift_l: u8,
513    /// High shift register (read out on bit 4).
514    pub(crate) shift_h: u8,
515    /// Last strobe level written (bit 0 of `$4016`).
516    pub(crate) strobe: bool,
517}
518
519impl PowerPadState {
520    /// New mat with no buttons pressed.
521    #[must_use]
522    pub const fn new() -> Self {
523        Self {
524            buttons: 0,
525            shift_l: 0,
526            shift_h: 0,
527            strobe: false,
528        }
529    }
530
531    /// Reload both shift registers from the live button mask (the parallel
532    /// latch). The bit order matches the `NESdev` / Mesen serial layout.
533    const fn reload(&mut self) {
534        // bit(i) = (buttons >> i) & 1, as u8. Inlined (no closures in const fn).
535        let p = self.buttons;
536        // L: buttons 2,1,5,9,6,10,11,7 (0-based 1,0,4,8,5,9,10,6).
537        self.shift_l = (((p >> 1) & 1) as u8)
538            | (((p & 1) as u8) << 1)
539            | ((((p >> 4) & 1) as u8) << 2)
540            | ((((p >> 8) & 1) as u8) << 3)
541            | ((((p >> 5) & 1) as u8) << 4)
542            | ((((p >> 9) & 1) as u8) << 5)
543            | ((((p >> 10) & 1) as u8) << 6)
544            | ((((p >> 6) & 1) as u8) << 7);
545        // H: buttons 4,3,12,8 (0-based 3,2,11,7), then four 1 bits (read as H=1).
546        self.shift_h = (((p >> 3) & 1) as u8)
547            | ((((p >> 2) & 1) as u8) << 1)
548            | ((((p >> 11) & 1) as u8) << 2)
549            | ((((p >> 7) & 1) as u8) << 3)
550            | 0xF0;
551    }
552
553    /// Update the live pressed-button mask (bit `i` = mat button `i+1`). While
554    /// the strobe is held high the registers track the live mask (parallel
555    /// load), matching the standard controller's latch-while-strobed semantics.
556    pub const fn set(&mut self, buttons: u16) {
557        self.buttons = buttons & 0x0FFF;
558        if self.strobe {
559            self.reload();
560        }
561    }
562
563    /// Handle a `$4016` strobe write. While bit 0 is high the registers are
564    /// (re)loaded from the live buttons; the falling edge leaves the latched
565    /// snapshot to shift out.
566    pub const fn write_strobe(&mut self, value: u8) {
567        let new_strobe = value & 1 != 0;
568        if new_strobe {
569            self.reload();
570        }
571        self.strobe = new_strobe;
572    }
573
574    /// Read the device byte for a `$4017` access, shifting both registers.
575    /// Bit 4 = the current serial-out (`LSb`) of register H, bit 3 = register L;
576    /// each read then shifts both right (feeding `1`s in from the top). The
577    /// caller ORs in the open-bus upper bits. While the strobe is high the
578    /// registers are continuously reloaded (reads return the first button).
579    pub const fn read(&mut self) -> u8 {
580        if self.strobe {
581            self.reload();
582        }
583        let out = ((self.shift_h & 1) << 4) | ((self.shift_l & 1) << 3);
584        self.shift_l = (self.shift_l >> 1) | 0x80;
585        self.shift_h = (self.shift_h >> 1) | 0x80;
586        out
587    }
588
589    /// Side-effect-free sample of the next device byte (debugger peek).
590    #[must_use]
591    pub const fn peek(&self) -> u8 {
592        ((self.shift_h & 1) << 4) | ((self.shift_l & 1) << 3)
593    }
594
595    /// Reconstruct from save-state parts. `buttons` is masked to the 12 mat
596    /// bits, matching [`Self::set`], so a malformed save-state cannot inject
597    /// out-of-range bits.
598    #[must_use]
599    pub const fn from_parts(buttons: u16, shift_l: u8, shift_h: u8, strobe: bool) -> Self {
600        Self {
601            buttons: buttons & 0x0FFF,
602            shift_l,
603            shift_h,
604            strobe,
605        }
606    }
607
608    /// Raw live button mask (save-state).
609    #[must_use]
610    pub const fn buttons_raw(&self) -> u16 {
611        self.buttons
612    }
613    /// Raw low shift register (save-state).
614    #[must_use]
615    pub const fn shift_l_raw(&self) -> u8 {
616        self.shift_l
617    }
618    /// Raw high shift register (save-state).
619    #[must_use]
620    pub const fn shift_h_raw(&self) -> u8 {
621        self.shift_h
622    }
623    /// Raw strobe state (save-state).
624    #[must_use]
625    pub const fn strobe_raw(&self) -> bool {
626        self.strobe
627    }
628}
629
630/// The (Hyperkin / Nintendo) mouse overlay state — the SNES-style serial mouse
631/// as wired to an NES `$4016`/`$4017` port (D0 serial-out).
632///
633/// Per the `NESdev` "Mouse" page (the SNES mouse, the canonical serial mouse
634/// reused on the NES), a strobe latches a fixed-format 32-bit report that is
635/// then shifted out **MSb-first on D0** (one bit per port read):
636///
637/// ```text
638/// bits 31..28 : signature 0b0001 (device id nibble)
639/// bits 27..26 : 00
640/// bits 25..24 : sensitivity (00 low / 01 medium / 10 high; cycled by pressing
641///               both buttons on real hardware — we expose it as a field)
642/// bit  23     : left button  (1 = pressed)
643/// bit  22     : right button (1 = pressed)
644/// bits 21..16 : 0
645/// bits 15..8  : Y movement — bit 15 = direction sign (1 = up/-), bits 14..8 =
646///               magnitude (0..127); 0 when not moving
647/// bits  7..0  : X movement — bit  7 = direction sign (1 = left/-), bits  6..0 =
648///               magnitude (0..127); 0 when not moving
649/// ```
650///
651/// After the 32 real bits are shifted out, further reads return `1` (the open
652/// serial line idles high), matching the standard controller's post-sequence
653/// behavior. Like the standard controller, while the strobe is held high the
654/// report is continuously re-latched, so reads return the first (signature) bit.
655#[derive(Clone, Copy, Debug, Default)]
656pub struct SnesMouseState {
657    /// Live delta-X this frame (signed; clamped into +/-127 on latch).
658    pub(crate) dx: i16,
659    /// Live delta-Y this frame (signed; clamped into +/-127 on latch).
660    pub(crate) dy: i16,
661    /// Left button held.
662    pub(crate) left: bool,
663    /// Right button held.
664    pub(crate) right: bool,
665    /// Sensitivity (0 low / 1 medium / 2 high). Reported in the latched word.
666    pub(crate) sensitivity: u8,
667    /// 32-bit shift register, MSb-first readout. Reloaded from the live state on
668    /// the strobe (the parallel latch).
669    pub(crate) shift: u32,
670    /// Count of real bits shifted out (0..=32); beyond 32, reads idle high (`1`).
671    pub(crate) read_count: u8,
672    /// Last strobe level written (bit 0 of `$4016`).
673    pub(crate) strobe: bool,
674}
675
676impl SnesMouseState {
677    /// New mouse at rest (no movement, buttons up, low sensitivity).
678    #[must_use]
679    pub const fn new() -> Self {
680        Self {
681            dx: 0,
682            dy: 0,
683            left: false,
684            right: false,
685            sensitivity: 0,
686            shift: 0,
687            read_count: 0,
688            strobe: false,
689        }
690    }
691
692    /// Encode one axis into the 8-bit serial field: bit 7 = direction sign
693    /// (1 = negative), bits 6..0 = magnitude clamped to 0..=127.
694    const fn enc_axis(v: i16) -> u32 {
695        // `v.unsigned_abs()` avoids the `-i16::MIN` overflow panic that `-v`
696        // would hit for `v == i16::MIN` (32768 is unrepresentable as `i16`).
697        let mag = v.unsigned_abs() as u32;
698        let mag = if mag > 127 { 127 } else { mag };
699        let sign = if v < 0 { 1u32 } else { 0 };
700        (sign << 7) | mag
701    }
702
703    /// Encode the current live state into the 32-bit report word (MSb-first
704    /// serial order; bit 31 is shifted out first).
705    const fn encode(&self) -> u32 {
706        let dx = Self::enc_axis(self.dx);
707        let dy = Self::enc_axis(self.dy);
708        let sig = 0b0001u32 << 28;
709        let sens = ((self.sensitivity & 0b11) as u32) << 24;
710        let left = (self.left as u32) << 23;
711        let right = (self.right as u32) << 22;
712        sig | sens | left | right | (dy << 8) | dx
713    }
714
715    /// Update the live movement + button + sensitivity state. Takes effect on
716    /// the next latch (strobe), matching the standard controller semantics.
717    pub const fn set(&mut self, dx: i16, dy: i16, left: bool, right: bool, sensitivity: u8) {
718        self.dx = dx;
719        self.dy = dy;
720        self.left = left;
721        self.right = right;
722        self.sensitivity = sensitivity & 0b11;
723        if self.strobe {
724            self.shift = self.encode();
725            self.read_count = 0;
726        }
727    }
728
729    /// Handle a `$4016` strobe write. On a high level the 32-bit report is
730    /// (re)latched from the live state; the read counter resets.
731    pub const fn write_strobe(&mut self, value: u8) {
732        let new_strobe = value & 1 != 0;
733        if new_strobe {
734            self.shift = self.encode();
735            self.read_count = 0;
736        }
737        self.strobe = new_strobe;
738    }
739
740    /// Read the device byte for a port access, shifting out one MSb-first bit on
741    /// D0. After 32 bits the line idles high (`1` on D0). While the strobe is
742    /// held high the report is continuously re-latched (reads return bit 31).
743    /// The caller ORs in the open-bus upper bits.
744    pub const fn read(&mut self) -> u8 {
745        if self.strobe {
746            self.shift = self.encode();
747            self.read_count = 0;
748        }
749        if self.read_count >= 32 {
750            return 1; // serial line idles high after the report
751        }
752        let bit = (self.shift >> 31) & 1;
753        self.shift <<= 1;
754        self.read_count += 1;
755        bit as u8
756    }
757
758    /// Side-effect-free sample of the next D0 bit (debugger peek).
759    #[must_use]
760    pub const fn peek(&self) -> u8 {
761        if self.read_count >= 32 {
762            return 1;
763        }
764        ((self.shift >> 31) & 1) as u8
765    }
766
767    /// Reconstruct from save-state parts.
768    #[must_use]
769    #[allow(clippy::too_many_arguments)] // one arg per persisted field
770    pub const fn from_parts(
771        dx: i16,
772        dy: i16,
773        left: bool,
774        right: bool,
775        sensitivity: u8,
776        shift: u32,
777        read_count: u8,
778        strobe: bool,
779    ) -> Self {
780        Self {
781            dx,
782            dy,
783            left,
784            right,
785            sensitivity: sensitivity & 0b11,
786            shift,
787            read_count,
788            strobe,
789        }
790    }
791
792    /// Raw delta-X (save-state).
793    #[must_use]
794    pub const fn dx_raw(&self) -> i16 {
795        self.dx
796    }
797    /// Raw delta-Y (save-state).
798    #[must_use]
799    pub const fn dy_raw(&self) -> i16 {
800        self.dy
801    }
802    /// Raw left button (save-state).
803    #[must_use]
804    pub const fn left_raw(&self) -> bool {
805        self.left
806    }
807    /// Raw right button (save-state).
808    #[must_use]
809    pub const fn right_raw(&self) -> bool {
810        self.right
811    }
812    /// Raw sensitivity (save-state).
813    #[must_use]
814    pub const fn sensitivity_raw(&self) -> u8 {
815        self.sensitivity
816    }
817    /// Raw shift register (save-state).
818    #[must_use]
819    pub const fn shift_raw(&self) -> u32 {
820        self.shift
821    }
822    /// Raw read counter (save-state).
823    #[must_use]
824    pub const fn read_count_raw(&self) -> u8 {
825        self.read_count
826    }
827    /// Raw strobe state (save-state).
828    #[must_use]
829    pub const fn strobe_raw(&self) -> bool {
830        self.strobe
831    }
832}
833
834/// Number of physical keys on the Famicom Family BASIC keyboard matrix
835/// (`9 rows x 8 columns / 2` halves; 72 keys, with a handful of unused matrix
836/// positions reported as `1` / not-pressed).
837pub const FAMILY_KEYBOARD_KEYS: usize = 72;
838
839/// Number of selectable rows in the Family BASIC keyboard matrix.
840const FAMILY_KEYBOARD_ROWS: usize = 9;
841
842/// The Famicom **Family BASIC keyboard** overlay state.
843///
844/// Per the `NESdev` "Family BASIC Keyboard" page, the keyboard is a `9 x 8`
845/// switch matrix (with the data-recorder lines unused here) read through the
846/// expansion port but software-visible on `$4017`. The protocol:
847///
848/// - **`$4016` write** — bit 0 (the "column" select; 0 selects the low 4 keys
849///   of the current row, 1 selects the high 4) and bit 1 (a clock; a 0->1
850///   transition advances to the next row). Bit 2 enables the keyboard matrix;
851///   when bit 2 is 0 the matrix is disabled and `$4017` reads `1`s. Writing
852///   bit 1 = 0 while bit 2 = 1 **resets** the row counter to 0.
853/// - **`$4017` read** — bits 4..1 carry the four key switches of the currently
854///   selected (row, column-half), **active-low** (0 = pressed). There are 9
855///   rows x 2 halves = 18 selectable groups of 4 keys = 72 key positions.
856///
857/// We model the live pressed state as a 72-bit key bitmap (`[u8; 9]`, one byte
858/// per row: low nibble = column-half 0, high nibble = column-half 1) and the
859/// row counter + column select per the write protocol. Determinism holds: it is
860/// a pure function of the writes + the live key bitmap.
861#[derive(Clone, Copy, Debug)]
862pub struct FamilyKeyboardState {
863    /// Per-row key bitmap. `keys[row]` bits 0..=3 = column-half 0 keys, bits
864    /// 4..=7 = column-half 1 keys. A set bit = that key is held.
865    pub(crate) keys: [u8; FAMILY_KEYBOARD_ROWS],
866    /// Current matrix row (0..=8); wraps/saturates at the last row.
867    pub(crate) row: u8,
868    /// Column-half select (bit 0 of the last `$4016` write): 0 = low nibble,
869    /// 1 = high nibble.
870    pub(crate) column: bool,
871    /// Whether the matrix is enabled (bit 2 of the last `$4016` write). When
872    /// disabled, `$4017` reads return all-`1` (no keys).
873    pub(crate) enabled: bool,
874    /// Last clock level (bit 1 of `$4016`); a 0->1 edge advances the row.
875    pub(crate) clock: bool,
876}
877
878impl Default for FamilyKeyboardState {
879    fn default() -> Self {
880        Self::new()
881    }
882}
883
884impl FamilyKeyboardState {
885    /// New keyboard with no keys held, matrix reset + disabled.
886    #[must_use]
887    pub const fn new() -> Self {
888        Self {
889            keys: [0; FAMILY_KEYBOARD_ROWS],
890            row: 0,
891            column: false,
892            enabled: false,
893            clock: false,
894        }
895    }
896
897    /// Set the full pressed-key bitmap (one byte per matrix row; low nibble =
898    /// column-half 0, high nibble = column-half 1). The frontend builds this
899    /// from host keys via its key map.
900    pub const fn set_keys(&mut self, keys: [u8; FAMILY_KEYBOARD_ROWS]) {
901        self.keys = keys;
902    }
903
904    /// Set one key by linear index (0..72) — `index = row * 8 + bit`, matching
905    /// the matrix layout (`bit` 0..=3 = column-half 0, 4..=7 = column-half 1).
906    /// Out-of-range indices are ignored.
907    pub const fn set_key(&mut self, index: usize, pressed: bool) {
908        if index >= FAMILY_KEYBOARD_KEYS {
909            return;
910        }
911        let row = index / 8;
912        #[allow(clippy::cast_possible_truncation)] // index % 8 is always 0..=7
913        let bit = (index % 8) as u8;
914        if pressed {
915            self.keys[row] |= 1 << bit;
916        } else {
917            self.keys[row] &= !(1 << bit);
918        }
919    }
920
921    /// Handle a `$4016` write: latch column select (bit 0), advance the row on a
922    /// clock (bit 1) rising edge, set the matrix-enable (bit 2). A clock low
923    /// while enabled resets the row counter to 0.
924    pub const fn write_strobe(&mut self, value: u8) {
925        let column = value & 0x01 != 0;
926        let clock = value & 0x02 != 0;
927        let enabled = value & 0x04 != 0;
928        if enabled {
929            if clock && !self.clock {
930                // Rising clock edge: advance to the next row (saturate at last).
931                if (self.row as usize) < FAMILY_KEYBOARD_ROWS - 1 {
932                    self.row += 1;
933                }
934            } else if !clock {
935                // Clock low (while enabled): reset to the first row.
936                self.row = 0;
937            }
938        }
939        self.column = column;
940        self.enabled = enabled;
941        self.clock = clock;
942    }
943
944    /// Read the device byte for a `$4017` access. The four selected key switches
945    /// are returned on bits 4..1, **active-low** (0 = pressed). When the matrix
946    /// is disabled, all four bits read `1` (no keys). The caller ORs in the
947    /// open-bus upper bits.
948    #[must_use]
949    pub const fn read(&self) -> u8 {
950        if !self.enabled {
951            // Disabled matrix: key switches all read high (not pressed).
952            return 0b0001_1110;
953        }
954        let row = self.row as usize;
955        let byte = self.keys[row];
956        let nibble = if self.column {
957            (byte >> 4) & 0x0F
958        } else {
959            byte & 0x0F
960        };
961        // Active-low: pressed key (1 in our bitmap) reads 0 on the wire.
962        let wire = (!nibble) & 0x0F;
963        wire << 1
964    }
965
966    /// Side-effect-free sample of the device byte (debugger peek) — identical to
967    /// [`Self::read`] (the keyboard read has no side effects).
968    #[must_use]
969    pub const fn peek(&self) -> u8 {
970        self.read()
971    }
972
973    /// Reconstruct from save-state parts.
974    #[must_use]
975    pub const fn from_parts(
976        keys: [u8; FAMILY_KEYBOARD_ROWS],
977        row: u8,
978        column: bool,
979        enabled: bool,
980        clock: bool,
981    ) -> Self {
982        // Clamp the restored row to the matrix bound: a corrupt/malicious
983        // save-state must not be able to drive `read()`'s `self.keys[row]`
984        // out of bounds. The live `write_strobe` path already saturates the
985        // row at `FAMILY_KEYBOARD_ROWS - 1`; mirror that on restore.
986        let row = if (row as usize) >= FAMILY_KEYBOARD_ROWS {
987            #[allow(clippy::cast_possible_truncation)] // ROWS is small (9)
988            {
989                (FAMILY_KEYBOARD_ROWS - 1) as u8
990            }
991        } else {
992            row
993        };
994        Self {
995            keys,
996            row,
997            column,
998            enabled,
999            clock,
1000        }
1001    }
1002
1003    /// Raw per-row key bitmap (save-state).
1004    #[must_use]
1005    pub const fn keys_raw(&self) -> [u8; FAMILY_KEYBOARD_ROWS] {
1006        self.keys
1007    }
1008    /// Raw row counter (save-state).
1009    #[must_use]
1010    pub const fn row_raw(&self) -> u8 {
1011        self.row
1012    }
1013    /// Raw column select (save-state).
1014    #[must_use]
1015    pub const fn column_raw(&self) -> bool {
1016        self.column
1017    }
1018    /// Raw matrix-enable (save-state).
1019    #[must_use]
1020    pub const fn enabled_raw(&self) -> bool {
1021        self.enabled
1022    }
1023    /// Raw clock level (save-state).
1024    #[must_use]
1025    pub const fn clock_raw(&self) -> bool {
1026        self.clock
1027    }
1028}
1029
1030/// The **Konami Hyper Shot** overlay state (v1.3.0 Workstream F1).
1031///
1032/// A simple 4-button expansion controller (two players, each with a Run and a
1033/// Jump button) used by _Hyper Olympic_ / _Hyper Sports_. Per the `NESdev`
1034/// "Konami Hyper Shot" page it is read in **parallel** on `$4017` (no shift
1035/// register), with `$4016` writes selecting which player's buttons are
1036/// enabled:
1037///
1038/// ```text
1039/// $4016 write:               $4017 read:
1040/// 7  bit  0                  7  bit  0
1041/// ---- ----                  ---- ----
1042/// xxxx xEFx                  xxxD CBAx
1043///       ||                      | |||
1044///       |+- 0 = enable P1          | ||+-- P1 Run
1045///       +-- 0 = enable P2          | |+--- P1 Jump
1046///                                  | +---- P2 Run
1047///                                  +------ P2 Jump
1048/// ```
1049///
1050/// The Jump/Run bits for a player read `0` while that player's enable bit
1051/// ($4016 bit 1 for P1, bit 2 for P2) is **set** (i.e. disabled). Determinism
1052/// holds: the read is a pure function of the live button mask + the last write.
1053#[derive(Clone, Copy, Debug, Default)]
1054pub struct KonamiHyperShotState {
1055    /// Live button mask: bit 0 = P1 Run, bit 1 = P1 Jump, bit 2 = P2 Run,
1056    /// bit 3 = P2 Jump.
1057    pub(crate) buttons: u8,
1058    /// `true` if P1's buttons are enabled (`$4016` bit 1 == 0).
1059    pub(crate) p1_enabled: bool,
1060    /// `true` if P2's buttons are enabled (`$4016` bit 2 == 0).
1061    pub(crate) p2_enabled: bool,
1062}
1063
1064impl KonamiHyperShotState {
1065    /// New controller with no buttons held and both players enabled (the
1066    /// power-on `$4016` write has not happened yet; enable is active-low, so the
1067    /// quiescent state matches a write of 0).
1068    #[must_use]
1069    pub const fn new() -> Self {
1070        Self {
1071            buttons: 0,
1072            p1_enabled: true,
1073            p2_enabled: true,
1074        }
1075    }
1076
1077    /// Set the live 4-button mask (bit 0 = P1 Run, 1 = P1 Jump, 2 = P2 Run,
1078    /// 3 = P2 Jump). Bits above 3 are ignored.
1079    pub const fn set(&mut self, buttons: u8) {
1080        self.buttons = buttons & 0x0F;
1081    }
1082
1083    /// Handle a `$4016` write: bit 1 = 0 enables P1, bit 2 = 0 enables P2
1084    /// (active-low).
1085    pub const fn write_strobe(&mut self, value: u8) {
1086        self.p1_enabled = value & 0x02 == 0;
1087        self.p2_enabled = value & 0x04 == 0;
1088    }
1089
1090    /// Read the device byte for a `$4017` access. Bit 1 = P1 Run, bit 2 = P1
1091    /// Jump, bit 3 = P2 Run, bit 4 = P2 Jump; a player's bits read `0` while
1092    /// disabled. The caller ORs in the open-bus upper bits.
1093    #[must_use]
1094    pub const fn read(&self) -> u8 {
1095        let p1_run = (self.buttons & 0x01 != 0) && self.p1_enabled;
1096        let p1_jump = (self.buttons & 0x02 != 0) && self.p1_enabled;
1097        let p2_run = (self.buttons & 0x04 != 0) && self.p2_enabled;
1098        let p2_jump = (self.buttons & 0x08 != 0) && self.p2_enabled;
1099        ((p1_run as u8) << 1)
1100            | ((p1_jump as u8) << 2)
1101            | ((p2_run as u8) << 3)
1102            | ((p2_jump as u8) << 4)
1103    }
1104
1105    /// Side-effect-free sample (debugger peek) — identical to [`Self::read`].
1106    #[must_use]
1107    pub const fn peek(&self) -> u8 {
1108        self.read()
1109    }
1110
1111    /// Reconstruct from save-state parts.
1112    #[must_use]
1113    pub const fn from_parts(buttons: u8, p1_enabled: bool, p2_enabled: bool) -> Self {
1114        Self {
1115            buttons: buttons & 0x0F,
1116            p1_enabled,
1117            p2_enabled,
1118        }
1119    }
1120
1121    /// Raw button mask (save-state).
1122    #[must_use]
1123    pub const fn buttons_raw(&self) -> u8 {
1124        self.buttons
1125    }
1126    /// Raw P1-enable (save-state).
1127    #[must_use]
1128    pub const fn p1_enabled_raw(&self) -> bool {
1129        self.p1_enabled
1130    }
1131    /// Raw P2-enable (save-state).
1132    #[must_use]
1133    pub const fn p2_enabled_raw(&self) -> bool {
1134        self.p2_enabled
1135    }
1136}
1137
1138/// The **Bandai Hyper Shot** (Exciting Boxing punching bag) overlay state
1139/// (v1.3.0 Workstream F1).
1140///
1141/// The punching bag has 8 sensors read on `$4017`, multiplexed by `$4016`
1142/// bit 1 (the "A" select) into two groups of four returned on bits 4..1. Per
1143/// the `NESdev` "Exciting Boxing Punching Bag" page:
1144///
1145/// ```text
1146/// $4016 write:               $4017 read:
1147/// 7  bit  0                  7  bit  0
1148/// ---- ----                  ---- ----
1149/// xxxx xxAx                  xxxE DCBx
1150///        |                      | |||
1151///        +- select group        | ||+-- Left Hook (A=0) / Left Jab  (A=1)
1152///                               | |+--- Move Right (A=0) / Body      (A=1)
1153///                               | +---- Move Left (A=0) / Right Jab  (A=1)
1154///                               +------ Right Hook (A=0) / Straight   (A=1)
1155/// ```
1156///
1157/// We model the 8 sensors as a live bitmask and the `A` select from the last
1158/// `$4016` write; the read is a pure function of both (deterministic).
1159#[derive(Clone, Copy, Debug, Default)]
1160pub struct BandaiHyperShotState {
1161    /// Live sensor mask. Group A=0 (bits 0..=3): Left Hook, Move Right, Move
1162    /// Left, Right Hook. Group A=1 (bits 4..=7): Left Jab, Body, Right Jab,
1163    /// Straight. A set bit = that sensor is active.
1164    pub(crate) sensors: u8,
1165    /// The `A` select latched from the last `$4016` write (bit 1). `false`
1166    /// selects the A=0 group (bits 0..=3), `true` the A=1 group (bits 4..=7).
1167    pub(crate) select: bool,
1168}
1169
1170impl BandaiHyperShotState {
1171    /// New punching bag with no sensor active, group A=0 selected.
1172    #[must_use]
1173    pub const fn new() -> Self {
1174        Self {
1175            sensors: 0,
1176            select: false,
1177        }
1178    }
1179
1180    /// Set the live 8-sensor mask. Bits 0..=3 are the A=0 group (Left Hook,
1181    /// Move Right, Move Left, Right Hook); bits 4..=7 are the A=1 group (Left
1182    /// Jab, Body, Right Jab, Straight).
1183    pub const fn set(&mut self, sensors: u8) {
1184        self.sensors = sensors;
1185    }
1186
1187    /// Handle a `$4016` write: bit 1 (`A`) selects which sensor group is
1188    /// returned on the next reads.
1189    pub const fn write_strobe(&mut self, value: u8) {
1190        self.select = value & 0x02 != 0;
1191    }
1192
1193    /// Read the device byte for a `$4017` access. The selected group's four
1194    /// sensors appear on bits 4..1. The caller ORs in the open-bus upper bits.
1195    #[must_use]
1196    pub const fn read(&self) -> u8 {
1197        let nibble = if self.select {
1198            (self.sensors >> 4) & 0x0F
1199        } else {
1200            self.sensors & 0x0F
1201        };
1202        nibble << 1
1203    }
1204
1205    /// Side-effect-free sample (debugger peek) — identical to [`Self::read`].
1206    #[must_use]
1207    pub const fn peek(&self) -> u8 {
1208        self.read()
1209    }
1210
1211    /// Reconstruct from save-state parts.
1212    #[must_use]
1213    pub const fn from_parts(sensors: u8, select: bool) -> Self {
1214        Self { sensors, select }
1215    }
1216
1217    /// Raw sensor mask (save-state).
1218    #[must_use]
1219    pub const fn sensors_raw(&self) -> u8 {
1220        self.sensors
1221    }
1222    /// Raw `A`-select (save-state).
1223    #[must_use]
1224    pub const fn select_raw(&self) -> bool {
1225        self.select
1226    }
1227}
1228
1229/// An optional non-standard device overlaid on a controller port. When set,
1230/// the bus's `$4016`/`$4017` read path returns this device's byte instead of
1231/// the standard controller / Four Score serial byte.
1232#[derive(Clone, Copy, Debug)]
1233pub enum InputDevice {
1234    /// NES Zapper light gun.
1235    Zapper(ZapperState),
1236    /// Arkanoid "Vaus" paddle.
1237    Vaus(VausState),
1238    /// NES Power Pad / Family Fun Fitness mat (12 buttons).
1239    PowerPad(PowerPadState),
1240    /// SNES-style serial mouse (Hyperkin / Nintendo), D0 serial-out.
1241    SnesMouse(SnesMouseState),
1242    /// Famicom Family BASIC keyboard (72-key matrix on `$4017`).
1243    FamilyKeyboard(FamilyKeyboardState),
1244    /// Bandai **Family Trainer** mat (v1.3.0 Workstream F1). Layout-equivalent
1245    /// to the [`PowerPad`](Self::PowerPad): the Famicom mat reuses the exact
1246    /// 12-button parallel-in/serial-out scan (it differs only in the expansion-
1247    /// port wiring vs the NES controller-port Power Pad), so the same
1248    /// [`PowerPadState`] drives it.
1249    FamilyTrainer(PowerPadState),
1250    /// **Subor keyboard** (v1.3.0 Workstream F1). A Family BASIC keyboard
1251    /// work-alike (the Subor clone matrix), reusing the same
1252    /// [`FamilyKeyboardState`] `9 x 8` matrix scan.
1253    SuborKeyboard(FamilyKeyboardState),
1254    /// **Konami Hyper Shot** (v1.3.0 Workstream F1): a 4-button (2-player
1255    /// Run/Jump) parallel-read expansion controller.
1256    KonamiHyperShot(KonamiHyperShotState),
1257    /// **Bandai Hyper Shot** / Exciting Boxing punching bag (v1.3.0 Workstream
1258    /// F1): an 8-sensor expansion controller multiplexed into two groups.
1259    BandaiHyperShot(BandaiHyperShotState),
1260}
1261
1262impl InputDevice {
1263    /// Forward a `$4016` strobe write to the device (only the Vaus latches on
1264    /// it; the Zapper ignores it).
1265    pub const fn write_strobe(&mut self, value: u8) {
1266        match self {
1267            Self::Vaus(v) => v.write_strobe(value),
1268            Self::PowerPad(p) | Self::FamilyTrainer(p) => p.write_strobe(value),
1269            Self::SnesMouse(m) => m.write_strobe(value),
1270            Self::FamilyKeyboard(k) | Self::SuborKeyboard(k) => k.write_strobe(value),
1271            Self::KonamiHyperShot(h) => h.write_strobe(value),
1272            Self::BandaiHyperShot(b) => b.write_strobe(value),
1273            Self::Zapper(_) => {}
1274        }
1275    }
1276
1277    /// Read the device byte (already bit-positioned), advancing any internal
1278    /// shift register.
1279    pub const fn read(&mut self) -> u8 {
1280        match self {
1281            Self::Vaus(v) => v.read(),
1282            Self::Zapper(z) => z.read(),
1283            Self::PowerPad(p) | Self::FamilyTrainer(p) => p.read(),
1284            Self::SnesMouse(m) => m.read(),
1285            Self::FamilyKeyboard(k) | Self::SuborKeyboard(k) => k.read(),
1286            Self::KonamiHyperShot(h) => h.read(),
1287            Self::BandaiHyperShot(b) => b.read(),
1288        }
1289    }
1290
1291    /// Side-effect-free sample of the device byte (debugger peek).
1292    #[must_use]
1293    pub const fn peek(&self) -> u8 {
1294        match self {
1295            Self::Vaus(v) => v.peek(),
1296            Self::Zapper(z) => z.read(),
1297            Self::PowerPad(p) | Self::FamilyTrainer(p) => p.peek(),
1298            Self::SnesMouse(m) => m.peek(),
1299            Self::FamilyKeyboard(k) | Self::SuborKeyboard(k) => k.peek(),
1300            Self::KonamiHyperShot(h) => h.peek(),
1301            Self::BandaiHyperShot(b) => b.peek(),
1302        }
1303    }
1304}
1305
1306#[cfg(test)]
1307mod tests {
1308    use super::*;
1309
1310    #[test]
1311    fn vaus_fire_button_on_bit3_independent_of_strobe() {
1312        let mut v = VausState::new();
1313        v.set(0x80, true);
1314        // No strobe yet; fire is returned directly regardless.
1315        assert_eq!(v.read() & (1 << 3), 1 << 3, "fire = bit 3 set");
1316        v.set(0x80, false);
1317        assert_eq!(v.read() & (1 << 3), 0, "fire released = bit 3 clear");
1318    }
1319
1320    #[test]
1321    fn vaus_knob_shifts_out_msb_first_inverted_on_bit4() {
1322        let mut v = VausState::new();
1323        // position 0b1010_0000: MSb-first raw bits = 1,0,1,0,0,0,0,0
1324        v.set(0b1010_0000, false);
1325        v.write_strobe(1);
1326        v.write_strobe(0);
1327        // Wire is inverted, so expected wire bits (bit 4) = 0,1,0,1,1,1,1,1.
1328        let expect_raw = [1u8, 0, 1, 0, 0, 0, 0, 0];
1329        for (i, raw) in expect_raw.iter().enumerate() {
1330            let byte = v.read();
1331            let wire_bit = (byte >> 4) & 1;
1332            assert_eq!(wire_bit, raw ^ 1, "read {i}: wire bit must be inverted raw");
1333        }
1334    }
1335
1336    #[test]
1337    fn vaus_post_empty_repeats_serial_in_bit() {
1338        let mut v = VausState::new();
1339        // LSb (serial-in) = 1 -> after the 8 real bits, reads repeat inverted 1 = 0.
1340        v.set(0b0000_0001, false);
1341        v.write_strobe(1);
1342        v.write_strobe(0);
1343        for _ in 0..8 {
1344            let _ = v.read();
1345        }
1346        // Now the register is all serial-in (1); wire bit = inverted = 0.
1347        for _ in 0..4 {
1348            assert_eq!((v.read() >> 4) & 1, 0);
1349        }
1350    }
1351
1352    #[test]
1353    fn zapper_light_detected_for_bright_region() {
1354        let mut z = ZapperState::new();
1355        z.set(10, 10, false);
1356        let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1357        // Bright white 3x3 target block centred on the aim point (10, 10) — the
1358        // target flash lights the whole photodiode aperture.
1359        for py in 9..=11usize {
1360            for px in 9..=11usize {
1361                let idx = (py * 256 + px) * 4;
1362                fb[idx] = 0xFF;
1363                fb[idx + 1] = 0xFF;
1364                fb[idx + 2] = 0xFF;
1365            }
1366        }
1367        z.sample_light(&fb);
1368        // Light detected -> bit 3 = 0.
1369        assert_eq!(
1370            z.read() & (1 << 3),
1371            0,
1372            "bright target region -> light detected (bit3=0)"
1373        );
1374    }
1375
1376    #[test]
1377    fn zapper_aperture_rejects_lone_bright_pixel() {
1378        // A single stray-bright pixel (below ZAPPER_APERTURE_MIN_BRIGHT) is not
1379        // enough to fire the photodiode — the aperture rejects PPU edge noise.
1380        let mut z = ZapperState::new();
1381        z.set(50, 50, false);
1382        let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1383        let idx = (50 * 256 + 50) * 4;
1384        fb[idx] = 0xFF;
1385        fb[idx + 1] = 0xFF;
1386        fb[idx + 2] = 0xFF;
1387        z.sample_light(&fb);
1388        assert_eq!(
1389            z.read() & (1 << 3),
1390            1 << 3,
1391            "lone bright pixel -> no light (bit3=1)"
1392        );
1393    }
1394
1395    #[test]
1396    fn zapper_no_light_for_dark_pixel() {
1397        let mut z = ZapperState::new();
1398        z.set(10, 10, false);
1399        let fb = alloc::vec![0u8; 256 * 240 * 4]; // all black
1400        z.sample_light(&fb);
1401        assert_eq!(
1402            z.read() & (1 << 3),
1403            1 << 3,
1404            "dark pixel -> no light (bit3=1)"
1405        );
1406    }
1407
1408    #[test]
1409    fn zapper_off_screen_never_sees_light() {
1410        let mut z = ZapperState::new();
1411        z.set(1000, 1000, false);
1412        let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1413        fb.fill(0xFF);
1414        z.sample_light(&fb);
1415        assert_eq!(z.read() & (1 << 3), 1 << 3, "off-screen aim -> no light");
1416    }
1417
1418    #[test]
1419    fn zapper_trigger_on_bit4() {
1420        let mut z = ZapperState::new();
1421        z.set(10, 10, true);
1422        assert_eq!(z.read() & (1 << 4), 1 << 4, "trigger pulled -> bit4 set");
1423        z.set(10, 10, false);
1424        assert_eq!(z.read() & (1 << 4), 0, "trigger released -> bit4 clear");
1425    }
1426
1427    /// Read 8 device bytes after a strobe, returning the bit-3 (L) and bit-4 (H)
1428    /// streams as bool arrays.
1429    fn powerpad_read8(p: &mut PowerPadState) -> ([bool; 8], [bool; 8]) {
1430        p.write_strobe(1);
1431        p.write_strobe(0);
1432        let mut l = [false; 8];
1433        let mut h = [false; 8];
1434        for i in 0..8 {
1435            let b = p.read();
1436            l[i] = b & (1 << 3) != 0;
1437            h[i] = b & (1 << 4) != 0;
1438        }
1439        (l, h)
1440    }
1441
1442    #[test]
1443    fn powerpad_no_buttons_reads_clear_then_h_ones() {
1444        // No buttons: L is all 0; H reads 0 for the first 4 (buttons 4,3,12,8),
1445        // then 1 for the trailing "read as H=1" bits.
1446        let mut p = PowerPadState::new();
1447        let (l, h) = powerpad_read8(&mut p);
1448        assert_eq!(l, [false; 8], "no L bits with nothing pressed");
1449        assert_eq!(h, [false, false, false, false, true, true, true, true]);
1450    }
1451
1452    #[test]
1453    fn powerpad_button_maps_to_expected_serial_position() {
1454        // Mat button "1" (index 0) is bit 1 of L -> appears on the 2nd read.
1455        let mut p = PowerPadState::new();
1456        p.set(1 << 0);
1457        let (l, _h) = powerpad_read8(&mut p);
1458        assert_eq!(l, [false, true, false, false, false, false, false, false]);
1459
1460        // Mat button "2" (index 1) is bit 0 of L -> appears on the 1st read.
1461        let mut p = PowerPadState::new();
1462        p.set(1 << 1);
1463        let (l, _h) = powerpad_read8(&mut p);
1464        assert_eq!(l, [true, false, false, false, false, false, false, false]);
1465
1466        // Mat button "4" (index 3) is bit 0 of H -> 1st read on bit 4.
1467        let mut p = PowerPadState::new();
1468        p.set(1 << 3);
1469        let (_l, h) = powerpad_read8(&mut p);
1470        assert_eq!(h, [true, false, false, false, true, true, true, true]);
1471    }
1472
1473    #[test]
1474    fn powerpad_strobe_high_reloads_each_read() {
1475        // While strobe is high, every read re-latches, so the first serial bit
1476        // is returned repeatedly (standard controller strobe semantics).
1477        let mut p = PowerPadState::new();
1478        p.set(1 << 1); // button "2" -> L bit 0 (1st-read position).
1479        p.write_strobe(1); // strobe held high
1480        for _ in 0..5 {
1481            assert_eq!(p.read() & (1 << 3), 1 << 3, "strobe-high repeats bit 0");
1482        }
1483    }
1484
1485    #[test]
1486    fn powerpad_save_state_round_trip() {
1487        let mut p = PowerPadState::new();
1488        p.set(0b1010_0101_0011);
1489        p.write_strobe(1);
1490        p.write_strobe(0);
1491        let _ = p.read(); // advance the registers
1492        let restored = PowerPadState::from_parts(
1493            p.buttons_raw(),
1494            p.shift_l_raw(),
1495            p.shift_h_raw(),
1496            p.strobe_raw(),
1497        );
1498        assert_eq!(restored.peek(), p.peek());
1499        assert_eq!(restored.buttons_raw(), 0b1010_0101_0011);
1500    }
1501
1502    #[test]
1503    fn powerpad_masks_to_12_bits() {
1504        // Bits above 11 are ignored (the mat has 12 buttons).
1505        let mut p = PowerPadState::new();
1506        p.set(0xFFFF);
1507        assert_eq!(p.buttons_raw(), 0x0FFF);
1508    }
1509
1510    #[test]
1511    fn input_device_enum_dispatch() {
1512        let mut d = InputDevice::Vaus(VausState::new());
1513        d.write_strobe(1);
1514        d.write_strobe(0);
1515        let _ = d.read();
1516        let mut z = InputDevice::Zapper(ZapperState::new());
1517        // Strobe is a no-op for the Zapper.
1518        z.write_strobe(1);
1519        assert_eq!(z.read() & (1 << 3), 1 << 3, "zapper: no light by default");
1520    }
1521
1522    /// Shift out `n` D0 bits (MSb-first), returning them packed into a u64 in
1523    /// read order (first bit = most significant of the returned `n`-bit value).
1524    fn mouse_read_bits(m: &mut SnesMouseState, n: usize) -> u64 {
1525        m.write_strobe(1);
1526        m.write_strobe(0);
1527        let mut acc = 0u64;
1528        for _ in 0..n {
1529            acc = (acc << 1) | u64::from(m.read() & 1);
1530        }
1531        acc
1532    }
1533
1534    #[test]
1535    fn snes_mouse_signature_nibble_is_0b0001() {
1536        let mut m = SnesMouseState::new();
1537        // First 4 bits are the device-id signature nibble 0b0001.
1538        let bits = mouse_read_bits(&mut m, 4);
1539        assert_eq!(bits, 0b0001, "signature nibble must be 0b0001");
1540    }
1541
1542    #[test]
1543    fn snes_mouse_full_report_encodes_buttons_and_movement() {
1544        let mut m = SnesMouseState::new();
1545        // dx = +5, dy = -3, left pressed, sensitivity = 2 (high).
1546        m.set(5, -3, true, false, 2);
1547        let word = mouse_read_bits(&mut m, 32);
1548        // Reconstruct the full 32-bit word and check each field.
1549        assert_eq!((word >> 28) & 0x0F, 0b0001, "signature");
1550        assert_eq!((word >> 24) & 0b11, 2, "sensitivity");
1551        assert_eq!((word >> 23) & 1, 1, "left button");
1552        assert_eq!((word >> 22) & 1, 0, "right button");
1553        // Y field (bits 15..8): sign=1 (negative), magnitude 3.
1554        let y = (word >> 8) & 0xFF;
1555        assert_eq!((y >> 7) & 1, 1, "Y sign negative");
1556        assert_eq!(y & 0x7F, 3, "Y magnitude");
1557        // X field (bits 7..0): sign=0 (positive), magnitude 5.
1558        let x = word & 0xFF;
1559        assert_eq!((x >> 7) & 1, 0, "X sign positive");
1560        assert_eq!(x & 0x7F, 5, "X magnitude");
1561    }
1562
1563    #[test]
1564    fn snes_mouse_idles_high_after_32_bits() {
1565        let mut m = SnesMouseState::new();
1566        m.write_strobe(1);
1567        m.write_strobe(0);
1568        for _ in 0..32 {
1569            let _ = m.read();
1570        }
1571        for _ in 0..4 {
1572            assert_eq!(m.read() & 1, 1, "serial line idles high after the report");
1573        }
1574    }
1575
1576    #[test]
1577    fn snes_mouse_clamps_movement_to_127() {
1578        let mut m = SnesMouseState::new();
1579        m.set(1000, -1000, false, false, 0);
1580        let word = mouse_read_bits(&mut m, 32);
1581        assert_eq!(word & 0x7F, 127, "X magnitude clamps to 127");
1582        assert_eq!((word >> 8) & 0x7F, 127, "Y magnitude clamps to 127");
1583    }
1584
1585    #[test]
1586    fn snes_mouse_enc_axis_handles_i16_extremes_without_panic() {
1587        // Regression: `enc_axis(i16::MIN)` must not panic on the `-v` overflow
1588        // (`-(-32768)` is unrepresentable as i16). Both extremes encode sanely:
1589        // sign bit set/clear and magnitude clamped to the 7-bit max of 127.
1590        let mut m = SnesMouseState::new();
1591        m.set(i16::MIN, i16::MAX, false, false, 0);
1592        let word = mouse_read_bits(&mut m, 32);
1593        // X = i16::MIN: negative -> sign bit (bit 7) set, magnitude clamped 127.
1594        assert_eq!(word & 0x80, 0x80, "i16::MIN encodes as negative");
1595        assert_eq!(word & 0x7F, 127, "i16::MIN magnitude clamps to 127");
1596        // Y = i16::MAX: positive -> sign bit clear, magnitude clamped 127.
1597        assert_eq!((word >> 8) & 0x80, 0, "i16::MAX encodes as positive");
1598        assert_eq!((word >> 8) & 0x7F, 127, "i16::MAX magnitude clamps to 127");
1599    }
1600
1601    #[test]
1602    fn snes_mouse_strobe_high_repeats_signature_bit() {
1603        let mut m = SnesMouseState::new();
1604        m.write_strobe(1); // held high
1605        for _ in 0..5 {
1606            // Signature MSb (bit 31 of 0b0001 << 28) is 0; repeats while strobed.
1607            assert_eq!(m.read() & 1, 0, "strobe-high repeats bit 31");
1608        }
1609    }
1610
1611    #[test]
1612    fn family_keyboard_disabled_reads_all_high() {
1613        let k = FamilyKeyboardState::new();
1614        // Not enabled (bit 2 unset): key switches all read high (bits 4..1 set).
1615        assert_eq!(
1616            k.read(),
1617            0b0001_1110,
1618            "disabled matrix -> no keys (bits 4..1=1)"
1619        );
1620    }
1621
1622    #[test]
1623    fn family_keyboard_pressed_key_reads_active_low() {
1624        let mut k = FamilyKeyboardState::new();
1625        // Press key at row 0, column-half 0, switch 0 (linear index 0).
1626        k.set_key(0, true);
1627        // Enable matrix (bit2), select column-half 0 (bit0=0), clock low resets row to 0.
1628        k.write_strobe(0b0000_0100);
1629        let r = k.read();
1630        // The pressed switch (bit 0 of the nibble) appears active-low on bit 1.
1631        assert_eq!(r & (1 << 1), 0, "pressed key reads 0 (active-low) on bit 1");
1632        // The other three switches are not pressed -> read 1.
1633        assert_eq!(r & (1 << 2), 1 << 2);
1634        assert_eq!(r & (1 << 3), 1 << 3);
1635        assert_eq!(r & (1 << 4), 1 << 4);
1636    }
1637
1638    #[test]
1639    fn family_keyboard_column_select_picks_high_nibble() {
1640        let mut k = FamilyKeyboardState::new();
1641        // Key at row 0, column-half 1, switch 0 = linear index 4 (row*8 + 4).
1642        k.set_key(4, true);
1643        // Enable + select column-half 1 (bit0=1), clock low (resets row to 0).
1644        k.write_strobe(0b0000_0101);
1645        let r = k.read();
1646        assert_eq!(
1647            r & (1 << 1),
1648            0,
1649            "column-half-1 key reads active-low on bit 1"
1650        );
1651        // Selecting column-half 0 instead shows nothing pressed there.
1652        k.write_strobe(0b0000_0100);
1653        assert_eq!(k.read() & (1 << 1), 1 << 1, "column-half 0 has no key here");
1654    }
1655
1656    #[test]
1657    fn family_keyboard_clock_edge_advances_row() {
1658        let mut k = FamilyKeyboardState::new();
1659        // Press a key on row 1, column-half 0, switch 0 = linear index 8.
1660        k.set_key(8, true);
1661        // Enable + clock low -> row 0.
1662        k.write_strobe(0b0000_0100);
1663        assert_eq!(k.read() & (1 << 1), 1 << 1, "row 0 has no key");
1664        // Rising clock edge (bit1 0->1) advances to row 1.
1665        k.write_strobe(0b0000_0110);
1666        assert_eq!(
1667            k.read() & (1 << 1),
1668            0,
1669            "row 1 key now selected (active-low)"
1670        );
1671    }
1672
1673    #[test]
1674    fn family_keyboard_save_state_round_trip() {
1675        let mut k = FamilyKeyboardState::new();
1676        k.set_key(8, true);
1677        k.set_key(40, true);
1678        k.write_strobe(0b0000_0110);
1679        let restored = FamilyKeyboardState::from_parts(
1680            k.keys_raw(),
1681            k.row_raw(),
1682            k.column_raw(),
1683            k.enabled_raw(),
1684            k.clock_raw(),
1685        );
1686        assert_eq!(restored.read(), k.read());
1687        assert_eq!(restored.keys_raw(), k.keys_raw());
1688    }
1689
1690    #[test]
1691    fn family_keyboard_from_parts_clamps_out_of_range_row() {
1692        // A corrupt/malicious save-state must not be able to drive a row value
1693        // that would index `self.keys[row]` out of bounds in `read()`.
1694        let keys = [0u8; FAMILY_KEYBOARD_ROWS];
1695        let restored = FamilyKeyboardState::from_parts(keys, 250, false, true, false);
1696        assert!(
1697            (restored.row_raw() as usize) < FAMILY_KEYBOARD_ROWS,
1698            "out-of-range row saturated to the matrix bound"
1699        );
1700        // Must not panic: enabled matrix indexes keys[row] in read().
1701        let _ = restored.read();
1702    }
1703
1704    #[test]
1705    fn family_keyboard_set_key_out_of_range_is_noop() {
1706        let mut k = FamilyKeyboardState::new();
1707        k.set_key(FAMILY_KEYBOARD_KEYS, true); // index == 72, out of range
1708        k.set_key(1000, true);
1709        assert_eq!(k.keys_raw(), [0; FAMILY_KEYBOARD_ROWS]);
1710    }
1711
1712    // --- v1.3.0 Workstream F1 — niche peripheral aliases + Hyper Shots ---
1713
1714    #[test]
1715    fn family_trainer_reuses_power_pad_scan() {
1716        // The Family Trainer is layout-equivalent to the Power Pad: an identical
1717        // PowerPadState must produce an identical serial readout through both
1718        // InputDevice variants.
1719        let mut pad = InputDevice::PowerPad(PowerPadState::new());
1720        let mut mat = InputDevice::FamilyTrainer(PowerPadState::new());
1721        if let (InputDevice::PowerPad(p), InputDevice::FamilyTrainer(m)) = (&mut pad, &mut mat) {
1722            p.set(0b1010_0101_0011);
1723            m.set(0b1010_0101_0011);
1724        }
1725        pad.write_strobe(1);
1726        pad.write_strobe(0);
1727        mat.write_strobe(1);
1728        mat.write_strobe(0);
1729        for i in 0..8 {
1730            assert_eq!(pad.read(), mat.read(), "read {i}: trainer == power pad");
1731        }
1732    }
1733
1734    #[test]
1735    fn subor_keyboard_reuses_family_keyboard_scan() {
1736        // The Subor keyboard reuses the Family BASIC keyboard matrix scan; the
1737        // same key state must read identically through both variants.
1738        let mut fam = FamilyKeyboardState::new();
1739        let mut sub = FamilyKeyboardState::new();
1740        fam.set_key(8, true);
1741        sub.set_key(8, true);
1742        let mut famd = InputDevice::FamilyKeyboard(fam);
1743        let mut subd = InputDevice::SuborKeyboard(sub);
1744        // Enable + clock low (row 0), then rising edge -> row 1.
1745        for v in [0b0000_0100u8, 0b0000_0110] {
1746            famd.write_strobe(v);
1747            subd.write_strobe(v);
1748        }
1749        assert_eq!(famd.read(), subd.read(), "subor == family keyboard read");
1750        assert_eq!(famd.peek(), subd.peek());
1751    }
1752
1753    #[test]
1754    fn konami_hyper_shot_buttons_on_expected_bits() {
1755        let mut h = KonamiHyperShotState::new();
1756        // P1 Run (bit0) -> read bit 1; P2 Jump (bit3) -> read bit 4.
1757        h.set(0b1001);
1758        h.write_strobe(0); // enable both players (active-low)
1759        let r = h.read();
1760        assert_eq!(r & (1 << 1), 1 << 1, "P1 Run on bit 1");
1761        assert_eq!(r & (1 << 4), 1 << 4, "P2 Jump on bit 4");
1762        assert_eq!(r & (1 << 2), 0, "P1 Jump not pressed");
1763        assert_eq!(r & (1 << 3), 0, "P2 Run not pressed");
1764    }
1765
1766    #[test]
1767    fn konami_hyper_shot_disabled_player_reads_zero() {
1768        let mut h = KonamiHyperShotState::new();
1769        h.set(0b1111); // all four buttons held
1770        // Disable P1 (bit 1 set), enable P2 (bit 2 clear).
1771        h.write_strobe(0b0000_0010);
1772        let r = h.read();
1773        assert_eq!(r & (1 << 1), 0, "disabled P1 Run reads 0");
1774        assert_eq!(r & (1 << 2), 0, "disabled P1 Jump reads 0");
1775        assert_eq!(r & (1 << 3), 1 << 3, "enabled P2 Run reads pressed");
1776        assert_eq!(r & (1 << 4), 1 << 4, "enabled P2 Jump reads pressed");
1777    }
1778
1779    #[test]
1780    fn konami_hyper_shot_save_state_round_trip() {
1781        let mut h = KonamiHyperShotState::new();
1782        h.set(0b0110);
1783        h.write_strobe(0b0000_0100); // disable P2
1784        let r = KonamiHyperShotState::from_parts(
1785            h.buttons_raw(),
1786            h.p1_enabled_raw(),
1787            h.p2_enabled_raw(),
1788        );
1789        assert_eq!(r.peek(), h.peek());
1790        assert_eq!(r.buttons_raw(), 0b0110);
1791    }
1792
1793    #[test]
1794    fn bandai_hyper_shot_select_picks_sensor_group() {
1795        let mut b = BandaiHyperShotState::new();
1796        // Group A=0 = bits 0..=3 (Left Hook = bit 0), A=1 = bits 4..=7.
1797        b.set(0b0001_0001); // Left Hook (group0) + Left Jab (group1)
1798        b.write_strobe(0); // A=0 group
1799        assert_eq!(b.read() & (1 << 1), 1 << 1, "group0 sensor 0 on bit 1");
1800        b.write_strobe(0b0000_0010); // A=1 group
1801        assert_eq!(b.read() & (1 << 1), 1 << 1, "group1 sensor 0 on bit 1");
1802        // A sensor only in group0 must vanish when the A=1 group is selected.
1803        let mut b2 = BandaiHyperShotState::new();
1804        b2.set(0b0000_1000); // only group0 bit 3 (Right Hook)
1805        b2.write_strobe(0b0000_0010); // select A=1
1806        assert_eq!(b2.read() & 0b1_1110, 0, "group0-only sensor absent in A=1");
1807    }
1808
1809    #[test]
1810    fn bandai_hyper_shot_save_state_round_trip() {
1811        let mut b = BandaiHyperShotState::new();
1812        b.set(0b1100_0011);
1813        b.write_strobe(0b0000_0010);
1814        let r = BandaiHyperShotState::from_parts(b.sensors_raw(), b.select_raw());
1815        assert_eq!(r.peek(), b.peek());
1816        assert_eq!(r.sensors_raw(), 0b1100_0011);
1817        assert!(r.select_raw());
1818    }
1819
1820    #[test]
1821    fn hyper_shots_dispatch_through_input_device() {
1822        let mut k = InputDevice::KonamiHyperShot(KonamiHyperShotState::new());
1823        k.write_strobe(0);
1824        let _ = k.read();
1825        let _ = k.peek();
1826        let mut bd = InputDevice::BandaiHyperShot(BandaiHyperShotState::new());
1827        bd.write_strobe(0);
1828        let _ = bd.read();
1829        let _ = bd.peek();
1830    }
1831
1832    // ---------------------------------------------------------------
1833    // A3 (v2.2.3): beam-relative temporal light integration.
1834    // ---------------------------------------------------------------
1835
1836    /// Build a framebuffer with a bright 3x3 target centred on `(x, y)`.
1837    /// Clamped at the edges so a target on row/column 0 is expressible (the
1838    /// pre-render regression test needs `y == 0`); the 3x3 aperture is simply
1839    /// clipped there, exactly as it is on hardware at the screen edge.
1840    fn fb_with_target(x: usize, y: usize) -> alloc::vec::Vec<u8> {
1841        let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1842        for py in y.saturating_sub(1)..=(y + 1).min(239) {
1843            for px in x.saturating_sub(1)..=(x + 1).min(255) {
1844                let idx = (py * 256 + px) * 4;
1845                fb[idx] = 0xFF;
1846                fb[idx + 1] = 0xFF;
1847                fb[idx + 2] = 0xFF;
1848            }
1849        }
1850        fb
1851    }
1852
1853    /// The core of A3: light is a function of WHERE THE BEAM IS, not merely of
1854    /// the frame. Before the beam paints the aim row there is no light, however
1855    /// bright the target; during the photodiode hold there is; after it drains
1856    /// there is not.
1857    #[test]
1858    fn zapper_temporal_light_follows_the_beam() {
1859        let mut z = ZapperState::new();
1860        z.set(100, 120, false);
1861        let fb = fb_with_target(100, 120);
1862
1863        // Beam still above the aim row: the row has not been painted yet.
1864        assert!(!z.light_at_scanline(&fb, 0));
1865        assert!(!z.light_at_scanline(&fb, 119));
1866
1867        // Beam reaches the aim row -> charged.
1868        assert!(z.light_at_scanline(&fb, 120));
1869        // Still within the ~19-26 scanline hold.
1870        assert!(z.light_at_scanline(&fb, 120 + ZAPPER_LIGHT_HOLD_SCANLINES - 1));
1871        // Drained.
1872        assert!(!z.light_at_scanline(&fb, 120 + ZAPPER_LIGHT_HOLD_SCANLINES));
1873        assert!(!z.light_at_scanline(&fb, 239));
1874    }
1875
1876    /// The frame-granular model cannot express the above: it reports the SAME
1877    /// answer at every scanline. This test is what makes A3 worth having.
1878    #[test]
1879    fn zapper_frame_model_is_scanline_invariant_but_temporal_is_not() {
1880        let mut z = ZapperState::new();
1881        z.set(100, 120, false);
1882        let fb = fb_with_target(100, 120);
1883        z.sample_light(&fb);
1884        // Frame model: one answer for the whole frame.
1885        assert!(z.light_seen);
1886        // Temporal model: three different answers within that same frame.
1887        let before = z.light_at_scanline(&fb, 10);
1888        let during = z.light_at_scanline(&fb, 125);
1889        let after = z.light_at_scanline(&fb, 200);
1890        assert!(
1891            !before && during && !after,
1892            "temporal model must vary within a frame"
1893        );
1894    }
1895
1896    /// The temporal path must reuse the SAME aperture rule, not a looser one:
1897    /// a lone bright pixel still fails to charge the sensor even at the exact
1898    /// beam position.
1899    #[test]
1900    fn zapper_temporal_rejects_lone_bright_pixel() {
1901        let mut z = ZapperState::new();
1902        z.set(50, 60, false);
1903        let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1904        let idx = (60 * 256 + 50) * 4;
1905        fb[idx] = 0xFF;
1906        fb[idx + 1] = 0xFF;
1907        fb[idx + 2] = 0xFF;
1908        assert!(
1909            !z.light_at_scanline(&fb, 60),
1910            "one pixel must not trip the aperture"
1911        );
1912        z.sample_light(&fb);
1913        assert!(!z.light_seen, "frame model must agree");
1914    }
1915
1916    /// Aiming off-screen never sees light at any beam position.
1917    #[test]
1918    fn zapper_temporal_off_screen_never_sees_light() {
1919        let mut z = ZapperState::new();
1920        z.set(300, 250, false);
1921        let fb = alloc::vec![0xFFu8; 256 * 240 * 4];
1922        for sl in [0u16, 120, 239] {
1923            assert!(!z.light_at_scanline(&fb, sl));
1924        }
1925    }
1926
1927    /// `read_at_scanline` carries the same inverted polarity and trigger bit as
1928    /// `read`, so a caller can swap models without re-deriving the byte format.
1929    #[test]
1930    fn zapper_temporal_read_byte_matches_the_documented_bit_layout() {
1931        let mut z = ZapperState::new();
1932        z.set(100, 120, true); // trigger pulled
1933        let fb = fb_with_target(100, 120);
1934        // In the hold: light detected -> bit 3 = 0; trigger -> bit 4 = 1.
1935        assert_eq!(z.read_at_scanline(&fb, 121) & 0b0001_1000, 0b0001_0000);
1936        // Drained: light NOT detected -> bit 3 = 1.
1937        assert_eq!(z.read_at_scanline(&fb, 200) & 0b0001_1000, 0b0001_1000);
1938    }
1939
1940    /// [`ZapperState::read_before_visible`] — the total-conversion fallback for a
1941    /// negative scanline — reports no light regardless of the framebuffer, and
1942    /// still carries the trigger bit.
1943    ///
1944    /// This pins the fallback's contract, not a live defect: `Ppu::scanline()`
1945    /// is non-negative on every region (pre-render is line 261 NTSC / 311 PAL,
1946    /// not -1), so the bus reaches this only if a future convention ever hands a
1947    /// negative scanline to `u16::try_from`. The paired assertion below shows the
1948    /// difference the fallback guards against — the real pre-render line, being
1949    /// past the photodiode hold window, already reads no-light through the normal
1950    /// `read_at_scanline` path.
1951    #[test]
1952    fn zapper_read_before_visible_reports_no_light() {
1953        let mut z = ZapperState::new();
1954        z.set(100, 0, true); // aim on visible row 0, trigger pulled
1955        let fb = fb_with_target(100, 0);
1956
1957        // v2.3.6: reading WHILE the beam is on the aim row reports no light —
1958        // the row is only part-way painted, so the phosphor has not emitted it
1959        // yet and the framebuffer still holds the previous frame there. (This
1960        // assertion read "light IS detected at row 0" until v2.3.6; sampling
1961        // rows the beam had not finished is what let the sensor report light on
1962        // a fully black screen, which made a Duck Hunt hit impossible. See
1963        // `aperture_is_bright_painted`.)
1964        assert_eq!(
1965            z.read_at_scanline(&fb, 0) & 0b0000_1000,
1966            0b0000_1000,
1967            "the aim row is still being painted at scanline == y: no light yet"
1968        );
1969
1970        // The first scanline PAST the aim row: the row is complete, the
1971        // photodiode is inside its hold window, so light IS detected. This is
1972        // the contrast the fallback below is measured against.
1973        assert_eq!(
1974            z.read_at_scanline(&fb, 1) & 0b0000_1000,
1975            0,
1976            "scanline 1 detects the light emitted by the completed row 0"
1977        );
1978
1979        // The real pre-render line (261 NTSC) is already no-light via the normal
1980        // path — it is past the hold window, so no fallback is needed there.
1981        assert_eq!(
1982            z.read_at_scanline(&fb, 261) & 0b0000_1000,
1983            0b0000_1000,
1984            "the real pre-render line (261) already reads no-light",
1985        );
1986
1987        // The negative-scanline fallback: no light regardless of framebuffer.
1988        assert_eq!(
1989            z.read_before_visible() & 0b0000_1000,
1990            0b0000_1000,
1991            "read_before_visible reports light NOT detected",
1992        );
1993        // ...and it still carries the trigger bit like every other read.
1994        assert_eq!(z.read_before_visible() & 0b0001_0000, 0b0001_0000);
1995    }
1996}