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}