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
207impl ZapperState {
208 /// New zapper aimed off-screen, trigger released, no light.
209 #[must_use]
210 pub const fn new() -> Self {
211 Self {
212 x: u16::MAX,
213 y: u16::MAX,
214 trigger: false,
215 light_seen: false,
216 }
217 }
218
219 /// Update the live aim point + trigger state.
220 pub const fn set(&mut self, x: u16, y: u16, trigger: bool) {
221 self.x = x;
222 self.y = y;
223 self.trigger = trigger;
224 }
225
226 /// Sample the framebuffer luminance at the aim point, setting `light_seen`
227 /// if the pixel is bright enough. `framebuffer` is the PPU's RGBA8 256x240
228 /// buffer. Called once per frame by the bus after the frame completes.
229 pub fn sample_light(&mut self, framebuffer: &[u8]) {
230 const W: usize = 256;
231 const H: usize = 240;
232 if (self.x as usize) >= W || (self.y as usize) >= H {
233 // Aimed off-screen: never sees light.
234 self.light_seen = false;
235 return;
236 }
237 let idx = ((self.y as usize) * W + (self.x as usize)) * 4;
238 // Guard against a partial framebuffer.
239 if idx + 2 >= framebuffer.len() {
240 self.light_seen = false;
241 return;
242 }
243 let r = u16::from(framebuffer[idx]);
244 let g = u16::from(framebuffer[idx + 1]);
245 let b = u16::from(framebuffer[idx + 2]);
246 // Rec.601 luma approximation (integer): (77*R + 150*G + 29*B) >> 8.
247 let luma = (77 * r + 150 * g + 29 * b) >> 8;
248 self.light_seen = luma >= ZAPPER_LUMA_THRESHOLD;
249 }
250
251 /// The device byte for a `$4016`/`$4017` access. Bit 3 = light (0 detected /
252 /// 1 not), bit 4 = trigger (1 pulled). Independent of the strobe (the
253 /// Zapper has no shift register). The caller ORs in the open-bus upper bits.
254 #[must_use]
255 pub const fn read(&self) -> u8 {
256 let light_not_detected = (!self.light_seen) as u8;
257 let trigger = self.trigger as u8;
258 (trigger << 4) | (light_not_detected << 3)
259 }
260
261 /// Reconstruct from save-state parts.
262 #[must_use]
263 pub const fn from_parts(x: u16, y: u16, trigger: bool, light_seen: bool) -> Self {
264 Self {
265 x,
266 y,
267 trigger,
268 light_seen,
269 }
270 }
271
272 /// Raw aim X (save-state).
273 #[must_use]
274 pub const fn x_raw(&self) -> u16 {
275 self.x
276 }
277 /// Raw aim Y (save-state).
278 #[must_use]
279 pub const fn y_raw(&self) -> u16 {
280 self.y
281 }
282 /// Raw trigger state (save-state).
283 #[must_use]
284 pub const fn trigger_raw(&self) -> bool {
285 self.trigger
286 }
287 /// Raw light-seen state (save-state).
288 #[must_use]
289 pub const fn light_seen_raw(&self) -> bool {
290 self.light_seen
291 }
292}
293
294/// The NES Power Pad (a.k.a. Family Fun Fitness / Family Trainer mat) overlay.
295///
296/// A 12-button mat read on the player-2 port (`$4017`) through two 8-bit
297/// parallel-in/serial-out shift registers (a pair of 4021s), strobed by the
298/// standard `$4016` controller strobe. The 12 buttons are indexed 0..=11
299/// (matching the mat's "1".."12" labels); the frontend decides which physical
300/// keys map to which mat button (and any Side-A/Side-B row inversion).
301///
302/// Per the `NESdev` "Power Pad" page (and Mesen's implementation), the button
303/// bits load into two registers and shift out LSb-first on bits 3 and 4 of each
304/// `$4017` read:
305///
306/// - register L (bit 3 of the read): buttons 2, 1, 5, 9, 6, 10, 11, 7;
307/// - register H (bit 4 of the read): buttons 4, 3, 12, 8, then four `1` bits.
308///
309/// (Button numbers are 1-based here, matching the mat labels; the code uses
310/// 0-based indices.) Each read shifts both registers right and feeds `1`s in
311/// from the top, so post-shift reads settle to "no button".
312#[derive(Clone, Copy, Debug, Default)]
313pub struct PowerPadState {
314 /// Live pressed-button mask: bit `i` (0..=11) set = mat button `i+1` held.
315 pub(crate) buttons: u16,
316 /// Low shift register (read out on bit 3).
317 pub(crate) shift_l: u8,
318 /// High shift register (read out on bit 4).
319 pub(crate) shift_h: u8,
320 /// Last strobe level written (bit 0 of `$4016`).
321 pub(crate) strobe: bool,
322}
323
324impl PowerPadState {
325 /// New mat with no buttons pressed.
326 #[must_use]
327 pub const fn new() -> Self {
328 Self {
329 buttons: 0,
330 shift_l: 0,
331 shift_h: 0,
332 strobe: false,
333 }
334 }
335
336 /// Reload both shift registers from the live button mask (the parallel
337 /// latch). The bit order matches the `NESdev` / Mesen serial layout.
338 const fn reload(&mut self) {
339 // bit(i) = (buttons >> i) & 1, as u8. Inlined (no closures in const fn).
340 let p = self.buttons;
341 // L: buttons 2,1,5,9,6,10,11,7 (0-based 1,0,4,8,5,9,10,6).
342 self.shift_l = (((p >> 1) & 1) as u8)
343 | (((p & 1) as u8) << 1)
344 | ((((p >> 4) & 1) as u8) << 2)
345 | ((((p >> 8) & 1) as u8) << 3)
346 | ((((p >> 5) & 1) as u8) << 4)
347 | ((((p >> 9) & 1) as u8) << 5)
348 | ((((p >> 10) & 1) as u8) << 6)
349 | ((((p >> 6) & 1) as u8) << 7);
350 // H: buttons 4,3,12,8 (0-based 3,2,11,7), then four 1 bits (read as H=1).
351 self.shift_h = (((p >> 3) & 1) as u8)
352 | ((((p >> 2) & 1) as u8) << 1)
353 | ((((p >> 11) & 1) as u8) << 2)
354 | ((((p >> 7) & 1) as u8) << 3)
355 | 0xF0;
356 }
357
358 /// Update the live pressed-button mask (bit `i` = mat button `i+1`). While
359 /// the strobe is held high the registers track the live mask (parallel
360 /// load), matching the standard controller's latch-while-strobed semantics.
361 pub const fn set(&mut self, buttons: u16) {
362 self.buttons = buttons & 0x0FFF;
363 if self.strobe {
364 self.reload();
365 }
366 }
367
368 /// Handle a `$4016` strobe write. While bit 0 is high the registers are
369 /// (re)loaded from the live buttons; the falling edge leaves the latched
370 /// snapshot to shift out.
371 pub const fn write_strobe(&mut self, value: u8) {
372 let new_strobe = value & 1 != 0;
373 if new_strobe {
374 self.reload();
375 }
376 self.strobe = new_strobe;
377 }
378
379 /// Read the device byte for a `$4017` access, shifting both registers.
380 /// Bit 4 = the current serial-out (`LSb`) of register H, bit 3 = register L;
381 /// each read then shifts both right (feeding `1`s in from the top). The
382 /// caller ORs in the open-bus upper bits. While the strobe is high the
383 /// registers are continuously reloaded (reads return the first button).
384 pub const fn read(&mut self) -> u8 {
385 if self.strobe {
386 self.reload();
387 }
388 let out = ((self.shift_h & 1) << 4) | ((self.shift_l & 1) << 3);
389 self.shift_l = (self.shift_l >> 1) | 0x80;
390 self.shift_h = (self.shift_h >> 1) | 0x80;
391 out
392 }
393
394 /// Side-effect-free sample of the next device byte (debugger peek).
395 #[must_use]
396 pub const fn peek(&self) -> u8 {
397 ((self.shift_h & 1) << 4) | ((self.shift_l & 1) << 3)
398 }
399
400 /// Reconstruct from save-state parts. `buttons` is masked to the 12 mat
401 /// bits, matching [`Self::set`], so a malformed save-state cannot inject
402 /// out-of-range bits.
403 #[must_use]
404 pub const fn from_parts(buttons: u16, shift_l: u8, shift_h: u8, strobe: bool) -> Self {
405 Self {
406 buttons: buttons & 0x0FFF,
407 shift_l,
408 shift_h,
409 strobe,
410 }
411 }
412
413 /// Raw live button mask (save-state).
414 #[must_use]
415 pub const fn buttons_raw(&self) -> u16 {
416 self.buttons
417 }
418 /// Raw low shift register (save-state).
419 #[must_use]
420 pub const fn shift_l_raw(&self) -> u8 {
421 self.shift_l
422 }
423 /// Raw high shift register (save-state).
424 #[must_use]
425 pub const fn shift_h_raw(&self) -> u8 {
426 self.shift_h
427 }
428 /// Raw strobe state (save-state).
429 #[must_use]
430 pub const fn strobe_raw(&self) -> bool {
431 self.strobe
432 }
433}
434
435/// The (Hyperkin / Nintendo) mouse overlay state — the SNES-style serial mouse
436/// as wired to an NES `$4016`/`$4017` port (D0 serial-out).
437///
438/// Per the `NESdev` "Mouse" page (the SNES mouse, the canonical serial mouse
439/// reused on the NES), a strobe latches a fixed-format 32-bit report that is
440/// then shifted out **MSb-first on D0** (one bit per port read):
441///
442/// ```text
443/// bits 31..28 : signature 0b0001 (device id nibble)
444/// bits 27..26 : 00
445/// bits 25..24 : sensitivity (00 low / 01 medium / 10 high; cycled by pressing
446/// both buttons on real hardware — we expose it as a field)
447/// bit 23 : left button (1 = pressed)
448/// bit 22 : right button (1 = pressed)
449/// bits 21..16 : 0
450/// bits 15..8 : Y movement — bit 15 = direction sign (1 = up/-), bits 14..8 =
451/// magnitude (0..127); 0 when not moving
452/// bits 7..0 : X movement — bit 7 = direction sign (1 = left/-), bits 6..0 =
453/// magnitude (0..127); 0 when not moving
454/// ```
455///
456/// After the 32 real bits are shifted out, further reads return `1` (the open
457/// serial line idles high), matching the standard controller's post-sequence
458/// behavior. Like the standard controller, while the strobe is held high the
459/// report is continuously re-latched, so reads return the first (signature) bit.
460#[derive(Clone, Copy, Debug, Default)]
461pub struct SnesMouseState {
462 /// Live delta-X this frame (signed; clamped into +/-127 on latch).
463 pub(crate) dx: i16,
464 /// Live delta-Y this frame (signed; clamped into +/-127 on latch).
465 pub(crate) dy: i16,
466 /// Left button held.
467 pub(crate) left: bool,
468 /// Right button held.
469 pub(crate) right: bool,
470 /// Sensitivity (0 low / 1 medium / 2 high). Reported in the latched word.
471 pub(crate) sensitivity: u8,
472 /// 32-bit shift register, MSb-first readout. Reloaded from the live state on
473 /// the strobe (the parallel latch).
474 pub(crate) shift: u32,
475 /// Count of real bits shifted out (0..=32); beyond 32, reads idle high (`1`).
476 pub(crate) read_count: u8,
477 /// Last strobe level written (bit 0 of `$4016`).
478 pub(crate) strobe: bool,
479}
480
481impl SnesMouseState {
482 /// New mouse at rest (no movement, buttons up, low sensitivity).
483 #[must_use]
484 pub const fn new() -> Self {
485 Self {
486 dx: 0,
487 dy: 0,
488 left: false,
489 right: false,
490 sensitivity: 0,
491 shift: 0,
492 read_count: 0,
493 strobe: false,
494 }
495 }
496
497 /// Encode one axis into the 8-bit serial field: bit 7 = direction sign
498 /// (1 = negative), bits 6..0 = magnitude clamped to 0..=127.
499 const fn enc_axis(v: i16) -> u32 {
500 // `v.unsigned_abs()` avoids the `-i16::MIN` overflow panic that `-v`
501 // would hit for `v == i16::MIN` (32768 is unrepresentable as `i16`).
502 let mag = v.unsigned_abs() as u32;
503 let mag = if mag > 127 { 127 } else { mag };
504 let sign = if v < 0 { 1u32 } else { 0 };
505 (sign << 7) | mag
506 }
507
508 /// Encode the current live state into the 32-bit report word (MSb-first
509 /// serial order; bit 31 is shifted out first).
510 const fn encode(&self) -> u32 {
511 let dx = Self::enc_axis(self.dx);
512 let dy = Self::enc_axis(self.dy);
513 let sig = 0b0001u32 << 28;
514 let sens = ((self.sensitivity & 0b11) as u32) << 24;
515 let left = (self.left as u32) << 23;
516 let right = (self.right as u32) << 22;
517 sig | sens | left | right | (dy << 8) | dx
518 }
519
520 /// Update the live movement + button + sensitivity state. Takes effect on
521 /// the next latch (strobe), matching the standard controller semantics.
522 pub const fn set(&mut self, dx: i16, dy: i16, left: bool, right: bool, sensitivity: u8) {
523 self.dx = dx;
524 self.dy = dy;
525 self.left = left;
526 self.right = right;
527 self.sensitivity = sensitivity & 0b11;
528 if self.strobe {
529 self.shift = self.encode();
530 self.read_count = 0;
531 }
532 }
533
534 /// Handle a `$4016` strobe write. On a high level the 32-bit report is
535 /// (re)latched from the live state; the read counter resets.
536 pub const fn write_strobe(&mut self, value: u8) {
537 let new_strobe = value & 1 != 0;
538 if new_strobe {
539 self.shift = self.encode();
540 self.read_count = 0;
541 }
542 self.strobe = new_strobe;
543 }
544
545 /// Read the device byte for a port access, shifting out one MSb-first bit on
546 /// D0. After 32 bits the line idles high (`1` on D0). While the strobe is
547 /// held high the report is continuously re-latched (reads return bit 31).
548 /// The caller ORs in the open-bus upper bits.
549 pub const fn read(&mut self) -> u8 {
550 if self.strobe {
551 self.shift = self.encode();
552 self.read_count = 0;
553 }
554 if self.read_count >= 32 {
555 return 1; // serial line idles high after the report
556 }
557 let bit = (self.shift >> 31) & 1;
558 self.shift <<= 1;
559 self.read_count += 1;
560 bit as u8
561 }
562
563 /// Side-effect-free sample of the next D0 bit (debugger peek).
564 #[must_use]
565 pub const fn peek(&self) -> u8 {
566 if self.read_count >= 32 {
567 return 1;
568 }
569 ((self.shift >> 31) & 1) as u8
570 }
571
572 /// Reconstruct from save-state parts.
573 #[must_use]
574 #[allow(clippy::too_many_arguments)] // one arg per persisted field
575 pub const fn from_parts(
576 dx: i16,
577 dy: i16,
578 left: bool,
579 right: bool,
580 sensitivity: u8,
581 shift: u32,
582 read_count: u8,
583 strobe: bool,
584 ) -> Self {
585 Self {
586 dx,
587 dy,
588 left,
589 right,
590 sensitivity: sensitivity & 0b11,
591 shift,
592 read_count,
593 strobe,
594 }
595 }
596
597 /// Raw delta-X (save-state).
598 #[must_use]
599 pub const fn dx_raw(&self) -> i16 {
600 self.dx
601 }
602 /// Raw delta-Y (save-state).
603 #[must_use]
604 pub const fn dy_raw(&self) -> i16 {
605 self.dy
606 }
607 /// Raw left button (save-state).
608 #[must_use]
609 pub const fn left_raw(&self) -> bool {
610 self.left
611 }
612 /// Raw right button (save-state).
613 #[must_use]
614 pub const fn right_raw(&self) -> bool {
615 self.right
616 }
617 /// Raw sensitivity (save-state).
618 #[must_use]
619 pub const fn sensitivity_raw(&self) -> u8 {
620 self.sensitivity
621 }
622 /// Raw shift register (save-state).
623 #[must_use]
624 pub const fn shift_raw(&self) -> u32 {
625 self.shift
626 }
627 /// Raw read counter (save-state).
628 #[must_use]
629 pub const fn read_count_raw(&self) -> u8 {
630 self.read_count
631 }
632 /// Raw strobe state (save-state).
633 #[must_use]
634 pub const fn strobe_raw(&self) -> bool {
635 self.strobe
636 }
637}
638
639/// Number of physical keys on the Famicom Family BASIC keyboard matrix
640/// (`9 rows x 8 columns / 2` halves; 72 keys, with a handful of unused matrix
641/// positions reported as `1` / not-pressed).
642pub const FAMILY_KEYBOARD_KEYS: usize = 72;
643
644/// Number of selectable rows in the Family BASIC keyboard matrix.
645const FAMILY_KEYBOARD_ROWS: usize = 9;
646
647/// The Famicom **Family BASIC keyboard** overlay state.
648///
649/// Per the `NESdev` "Family BASIC Keyboard" page, the keyboard is a `9 x 8`
650/// switch matrix (with the data-recorder lines unused here) read through the
651/// expansion port but software-visible on `$4017`. The protocol:
652///
653/// - **`$4016` write** — bit 0 (the "column" select; 0 selects the low 4 keys
654/// of the current row, 1 selects the high 4) and bit 1 (a clock; a 0->1
655/// transition advances to the next row). Bit 2 enables the keyboard matrix;
656/// when bit 2 is 0 the matrix is disabled and `$4017` reads `1`s. Writing
657/// bit 1 = 0 while bit 2 = 1 **resets** the row counter to 0.
658/// - **`$4017` read** — bits 4..1 carry the four key switches of the currently
659/// selected (row, column-half), **active-low** (0 = pressed). There are 9
660/// rows x 2 halves = 18 selectable groups of 4 keys = 72 key positions.
661///
662/// We model the live pressed state as a 72-bit key bitmap (`[u8; 9]`, one byte
663/// per row: low nibble = column-half 0, high nibble = column-half 1) and the
664/// row counter + column select per the write protocol. Determinism holds: it is
665/// a pure function of the writes + the live key bitmap.
666#[derive(Clone, Copy, Debug)]
667pub struct FamilyKeyboardState {
668 /// Per-row key bitmap. `keys[row]` bits 0..=3 = column-half 0 keys, bits
669 /// 4..=7 = column-half 1 keys. A set bit = that key is held.
670 pub(crate) keys: [u8; FAMILY_KEYBOARD_ROWS],
671 /// Current matrix row (0..=8); wraps/saturates at the last row.
672 pub(crate) row: u8,
673 /// Column-half select (bit 0 of the last `$4016` write): 0 = low nibble,
674 /// 1 = high nibble.
675 pub(crate) column: bool,
676 /// Whether the matrix is enabled (bit 2 of the last `$4016` write). When
677 /// disabled, `$4017` reads return all-`1` (no keys).
678 pub(crate) enabled: bool,
679 /// Last clock level (bit 1 of `$4016`); a 0->1 edge advances the row.
680 pub(crate) clock: bool,
681}
682
683impl Default for FamilyKeyboardState {
684 fn default() -> Self {
685 Self::new()
686 }
687}
688
689impl FamilyKeyboardState {
690 /// New keyboard with no keys held, matrix reset + disabled.
691 #[must_use]
692 pub const fn new() -> Self {
693 Self {
694 keys: [0; FAMILY_KEYBOARD_ROWS],
695 row: 0,
696 column: false,
697 enabled: false,
698 clock: false,
699 }
700 }
701
702 /// Set the full pressed-key bitmap (one byte per matrix row; low nibble =
703 /// column-half 0, high nibble = column-half 1). The frontend builds this
704 /// from host keys via its key map.
705 pub const fn set_keys(&mut self, keys: [u8; FAMILY_KEYBOARD_ROWS]) {
706 self.keys = keys;
707 }
708
709 /// Set one key by linear index (0..72) — `index = row * 8 + bit`, matching
710 /// the matrix layout (`bit` 0..=3 = column-half 0, 4..=7 = column-half 1).
711 /// Out-of-range indices are ignored.
712 pub const fn set_key(&mut self, index: usize, pressed: bool) {
713 if index >= FAMILY_KEYBOARD_KEYS {
714 return;
715 }
716 let row = index / 8;
717 #[allow(clippy::cast_possible_truncation)] // index % 8 is always 0..=7
718 let bit = (index % 8) as u8;
719 if pressed {
720 self.keys[row] |= 1 << bit;
721 } else {
722 self.keys[row] &= !(1 << bit);
723 }
724 }
725
726 /// Handle a `$4016` write: latch column select (bit 0), advance the row on a
727 /// clock (bit 1) rising edge, set the matrix-enable (bit 2). A clock low
728 /// while enabled resets the row counter to 0.
729 pub const fn write_strobe(&mut self, value: u8) {
730 let column = value & 0x01 != 0;
731 let clock = value & 0x02 != 0;
732 let enabled = value & 0x04 != 0;
733 if enabled {
734 if clock && !self.clock {
735 // Rising clock edge: advance to the next row (saturate at last).
736 if (self.row as usize) < FAMILY_KEYBOARD_ROWS - 1 {
737 self.row += 1;
738 }
739 } else if !clock {
740 // Clock low (while enabled): reset to the first row.
741 self.row = 0;
742 }
743 }
744 self.column = column;
745 self.enabled = enabled;
746 self.clock = clock;
747 }
748
749 /// Read the device byte for a `$4017` access. The four selected key switches
750 /// are returned on bits 4..1, **active-low** (0 = pressed). When the matrix
751 /// is disabled, all four bits read `1` (no keys). The caller ORs in the
752 /// open-bus upper bits.
753 #[must_use]
754 pub const fn read(&self) -> u8 {
755 if !self.enabled {
756 // Disabled matrix: key switches all read high (not pressed).
757 return 0b0001_1110;
758 }
759 let row = self.row as usize;
760 let byte = self.keys[row];
761 let nibble = if self.column {
762 (byte >> 4) & 0x0F
763 } else {
764 byte & 0x0F
765 };
766 // Active-low: pressed key (1 in our bitmap) reads 0 on the wire.
767 let wire = (!nibble) & 0x0F;
768 wire << 1
769 }
770
771 /// Side-effect-free sample of the device byte (debugger peek) — identical to
772 /// [`Self::read`] (the keyboard read has no side effects).
773 #[must_use]
774 pub const fn peek(&self) -> u8 {
775 self.read()
776 }
777
778 /// Reconstruct from save-state parts.
779 #[must_use]
780 pub const fn from_parts(
781 keys: [u8; FAMILY_KEYBOARD_ROWS],
782 row: u8,
783 column: bool,
784 enabled: bool,
785 clock: bool,
786 ) -> Self {
787 // Clamp the restored row to the matrix bound: a corrupt/malicious
788 // save-state must not be able to drive `read()`'s `self.keys[row]`
789 // out of bounds. The live `write_strobe` path already saturates the
790 // row at `FAMILY_KEYBOARD_ROWS - 1`; mirror that on restore.
791 let row = if (row as usize) >= FAMILY_KEYBOARD_ROWS {
792 #[allow(clippy::cast_possible_truncation)] // ROWS is small (9)
793 {
794 (FAMILY_KEYBOARD_ROWS - 1) as u8
795 }
796 } else {
797 row
798 };
799 Self {
800 keys,
801 row,
802 column,
803 enabled,
804 clock,
805 }
806 }
807
808 /// Raw per-row key bitmap (save-state).
809 #[must_use]
810 pub const fn keys_raw(&self) -> [u8; FAMILY_KEYBOARD_ROWS] {
811 self.keys
812 }
813 /// Raw row counter (save-state).
814 #[must_use]
815 pub const fn row_raw(&self) -> u8 {
816 self.row
817 }
818 /// Raw column select (save-state).
819 #[must_use]
820 pub const fn column_raw(&self) -> bool {
821 self.column
822 }
823 /// Raw matrix-enable (save-state).
824 #[must_use]
825 pub const fn enabled_raw(&self) -> bool {
826 self.enabled
827 }
828 /// Raw clock level (save-state).
829 #[must_use]
830 pub const fn clock_raw(&self) -> bool {
831 self.clock
832 }
833}
834
835/// The **Konami Hyper Shot** overlay state (v1.3.0 Workstream F1).
836///
837/// A simple 4-button expansion controller (two players, each with a Run and a
838/// Jump button) used by _Hyper Olympic_ / _Hyper Sports_. Per the `NESdev`
839/// "Konami Hyper Shot" page it is read in **parallel** on `$4017` (no shift
840/// register), with `$4016` writes selecting which player's buttons are
841/// enabled:
842///
843/// ```text
844/// $4016 write: $4017 read:
845/// 7 bit 0 7 bit 0
846/// ---- ---- ---- ----
847/// xxxx xEFx xxxD CBAx
848/// || | |||
849/// |+- 0 = enable P1 | ||+-- P1 Run
850/// +-- 0 = enable P2 | |+--- P1 Jump
851/// | +---- P2 Run
852/// +------ P2 Jump
853/// ```
854///
855/// The Jump/Run bits for a player read `0` while that player's enable bit
856/// ($4016 bit 1 for P1, bit 2 for P2) is **set** (i.e. disabled). Determinism
857/// holds: the read is a pure function of the live button mask + the last write.
858#[derive(Clone, Copy, Debug, Default)]
859pub struct KonamiHyperShotState {
860 /// Live button mask: bit 0 = P1 Run, bit 1 = P1 Jump, bit 2 = P2 Run,
861 /// bit 3 = P2 Jump.
862 pub(crate) buttons: u8,
863 /// `true` if P1's buttons are enabled (`$4016` bit 1 == 0).
864 pub(crate) p1_enabled: bool,
865 /// `true` if P2's buttons are enabled (`$4016` bit 2 == 0).
866 pub(crate) p2_enabled: bool,
867}
868
869impl KonamiHyperShotState {
870 /// New controller with no buttons held and both players enabled (the
871 /// power-on `$4016` write has not happened yet; enable is active-low, so the
872 /// quiescent state matches a write of 0).
873 #[must_use]
874 pub const fn new() -> Self {
875 Self {
876 buttons: 0,
877 p1_enabled: true,
878 p2_enabled: true,
879 }
880 }
881
882 /// Set the live 4-button mask (bit 0 = P1 Run, 1 = P1 Jump, 2 = P2 Run,
883 /// 3 = P2 Jump). Bits above 3 are ignored.
884 pub const fn set(&mut self, buttons: u8) {
885 self.buttons = buttons & 0x0F;
886 }
887
888 /// Handle a `$4016` write: bit 1 = 0 enables P1, bit 2 = 0 enables P2
889 /// (active-low).
890 pub const fn write_strobe(&mut self, value: u8) {
891 self.p1_enabled = value & 0x02 == 0;
892 self.p2_enabled = value & 0x04 == 0;
893 }
894
895 /// Read the device byte for a `$4017` access. Bit 1 = P1 Run, bit 2 = P1
896 /// Jump, bit 3 = P2 Run, bit 4 = P2 Jump; a player's bits read `0` while
897 /// disabled. The caller ORs in the open-bus upper bits.
898 #[must_use]
899 pub const fn read(&self) -> u8 {
900 let p1_run = (self.buttons & 0x01 != 0) && self.p1_enabled;
901 let p1_jump = (self.buttons & 0x02 != 0) && self.p1_enabled;
902 let p2_run = (self.buttons & 0x04 != 0) && self.p2_enabled;
903 let p2_jump = (self.buttons & 0x08 != 0) && self.p2_enabled;
904 ((p1_run as u8) << 1)
905 | ((p1_jump as u8) << 2)
906 | ((p2_run as u8) << 3)
907 | ((p2_jump as u8) << 4)
908 }
909
910 /// Side-effect-free sample (debugger peek) — identical to [`Self::read`].
911 #[must_use]
912 pub const fn peek(&self) -> u8 {
913 self.read()
914 }
915
916 /// Reconstruct from save-state parts.
917 #[must_use]
918 pub const fn from_parts(buttons: u8, p1_enabled: bool, p2_enabled: bool) -> Self {
919 Self {
920 buttons: buttons & 0x0F,
921 p1_enabled,
922 p2_enabled,
923 }
924 }
925
926 /// Raw button mask (save-state).
927 #[must_use]
928 pub const fn buttons_raw(&self) -> u8 {
929 self.buttons
930 }
931 /// Raw P1-enable (save-state).
932 #[must_use]
933 pub const fn p1_enabled_raw(&self) -> bool {
934 self.p1_enabled
935 }
936 /// Raw P2-enable (save-state).
937 #[must_use]
938 pub const fn p2_enabled_raw(&self) -> bool {
939 self.p2_enabled
940 }
941}
942
943/// The **Bandai Hyper Shot** (Exciting Boxing punching bag) overlay state
944/// (v1.3.0 Workstream F1).
945///
946/// The punching bag has 8 sensors read on `$4017`, multiplexed by `$4016`
947/// bit 1 (the "A" select) into two groups of four returned on bits 4..1. Per
948/// the `NESdev` "Exciting Boxing Punching Bag" page:
949///
950/// ```text
951/// $4016 write: $4017 read:
952/// 7 bit 0 7 bit 0
953/// ---- ---- ---- ----
954/// xxxx xxAx xxxE DCBx
955/// | | |||
956/// +- select group | ||+-- Left Hook (A=0) / Left Jab (A=1)
957/// | |+--- Move Right (A=0) / Body (A=1)
958/// | +---- Move Left (A=0) / Right Jab (A=1)
959/// +------ Right Hook (A=0) / Straight (A=1)
960/// ```
961///
962/// We model the 8 sensors as a live bitmask and the `A` select from the last
963/// `$4016` write; the read is a pure function of both (deterministic).
964#[derive(Clone, Copy, Debug, Default)]
965pub struct BandaiHyperShotState {
966 /// Live sensor mask. Group A=0 (bits 0..=3): Left Hook, Move Right, Move
967 /// Left, Right Hook. Group A=1 (bits 4..=7): Left Jab, Body, Right Jab,
968 /// Straight. A set bit = that sensor is active.
969 pub(crate) sensors: u8,
970 /// The `A` select latched from the last `$4016` write (bit 1). `false`
971 /// selects the A=0 group (bits 0..=3), `true` the A=1 group (bits 4..=7).
972 pub(crate) select: bool,
973}
974
975impl BandaiHyperShotState {
976 /// New punching bag with no sensor active, group A=0 selected.
977 #[must_use]
978 pub const fn new() -> Self {
979 Self {
980 sensors: 0,
981 select: false,
982 }
983 }
984
985 /// Set the live 8-sensor mask. Bits 0..=3 are the A=0 group (Left Hook,
986 /// Move Right, Move Left, Right Hook); bits 4..=7 are the A=1 group (Left
987 /// Jab, Body, Right Jab, Straight).
988 pub const fn set(&mut self, sensors: u8) {
989 self.sensors = sensors;
990 }
991
992 /// Handle a `$4016` write: bit 1 (`A`) selects which sensor group is
993 /// returned on the next reads.
994 pub const fn write_strobe(&mut self, value: u8) {
995 self.select = value & 0x02 != 0;
996 }
997
998 /// Read the device byte for a `$4017` access. The selected group's four
999 /// sensors appear on bits 4..1. The caller ORs in the open-bus upper bits.
1000 #[must_use]
1001 pub const fn read(&self) -> u8 {
1002 let nibble = if self.select {
1003 (self.sensors >> 4) & 0x0F
1004 } else {
1005 self.sensors & 0x0F
1006 };
1007 nibble << 1
1008 }
1009
1010 /// Side-effect-free sample (debugger peek) — identical to [`Self::read`].
1011 #[must_use]
1012 pub const fn peek(&self) -> u8 {
1013 self.read()
1014 }
1015
1016 /// Reconstruct from save-state parts.
1017 #[must_use]
1018 pub const fn from_parts(sensors: u8, select: bool) -> Self {
1019 Self { sensors, select }
1020 }
1021
1022 /// Raw sensor mask (save-state).
1023 #[must_use]
1024 pub const fn sensors_raw(&self) -> u8 {
1025 self.sensors
1026 }
1027 /// Raw `A`-select (save-state).
1028 #[must_use]
1029 pub const fn select_raw(&self) -> bool {
1030 self.select
1031 }
1032}
1033
1034/// An optional non-standard device overlaid on a controller port. When set,
1035/// the bus's `$4016`/`$4017` read path returns this device's byte instead of
1036/// the standard controller / Four Score serial byte.
1037#[derive(Clone, Copy, Debug)]
1038pub enum InputDevice {
1039 /// NES Zapper light gun.
1040 Zapper(ZapperState),
1041 /// Arkanoid "Vaus" paddle.
1042 Vaus(VausState),
1043 /// NES Power Pad / Family Fun Fitness mat (12 buttons).
1044 PowerPad(PowerPadState),
1045 /// SNES-style serial mouse (Hyperkin / Nintendo), D0 serial-out.
1046 SnesMouse(SnesMouseState),
1047 /// Famicom Family BASIC keyboard (72-key matrix on `$4017`).
1048 FamilyKeyboard(FamilyKeyboardState),
1049 /// Bandai **Family Trainer** mat (v1.3.0 Workstream F1). Layout-equivalent
1050 /// to the [`PowerPad`](Self::PowerPad): the Famicom mat reuses the exact
1051 /// 12-button parallel-in/serial-out scan (it differs only in the expansion-
1052 /// port wiring vs the NES controller-port Power Pad), so the same
1053 /// [`PowerPadState`] drives it.
1054 FamilyTrainer(PowerPadState),
1055 /// **Subor keyboard** (v1.3.0 Workstream F1). A Family BASIC keyboard
1056 /// work-alike (the Subor clone matrix), reusing the same
1057 /// [`FamilyKeyboardState`] `9 x 8` matrix scan.
1058 SuborKeyboard(FamilyKeyboardState),
1059 /// **Konami Hyper Shot** (v1.3.0 Workstream F1): a 4-button (2-player
1060 /// Run/Jump) parallel-read expansion controller.
1061 KonamiHyperShot(KonamiHyperShotState),
1062 /// **Bandai Hyper Shot** / Exciting Boxing punching bag (v1.3.0 Workstream
1063 /// F1): an 8-sensor expansion controller multiplexed into two groups.
1064 BandaiHyperShot(BandaiHyperShotState),
1065}
1066
1067impl InputDevice {
1068 /// Forward a `$4016` strobe write to the device (only the Vaus latches on
1069 /// it; the Zapper ignores it).
1070 pub const fn write_strobe(&mut self, value: u8) {
1071 match self {
1072 Self::Vaus(v) => v.write_strobe(value),
1073 Self::PowerPad(p) | Self::FamilyTrainer(p) => p.write_strobe(value),
1074 Self::SnesMouse(m) => m.write_strobe(value),
1075 Self::FamilyKeyboard(k) | Self::SuborKeyboard(k) => k.write_strobe(value),
1076 Self::KonamiHyperShot(h) => h.write_strobe(value),
1077 Self::BandaiHyperShot(b) => b.write_strobe(value),
1078 Self::Zapper(_) => {}
1079 }
1080 }
1081
1082 /// Read the device byte (already bit-positioned), advancing any internal
1083 /// shift register.
1084 pub const fn read(&mut self) -> u8 {
1085 match self {
1086 Self::Vaus(v) => v.read(),
1087 Self::Zapper(z) => z.read(),
1088 Self::PowerPad(p) | Self::FamilyTrainer(p) => p.read(),
1089 Self::SnesMouse(m) => m.read(),
1090 Self::FamilyKeyboard(k) | Self::SuborKeyboard(k) => k.read(),
1091 Self::KonamiHyperShot(h) => h.read(),
1092 Self::BandaiHyperShot(b) => b.read(),
1093 }
1094 }
1095
1096 /// Side-effect-free sample of the device byte (debugger peek).
1097 #[must_use]
1098 pub const fn peek(&self) -> u8 {
1099 match self {
1100 Self::Vaus(v) => v.peek(),
1101 Self::Zapper(z) => z.read(),
1102 Self::PowerPad(p) | Self::FamilyTrainer(p) => p.peek(),
1103 Self::SnesMouse(m) => m.peek(),
1104 Self::FamilyKeyboard(k) | Self::SuborKeyboard(k) => k.peek(),
1105 Self::KonamiHyperShot(h) => h.peek(),
1106 Self::BandaiHyperShot(b) => b.peek(),
1107 }
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114
1115 #[test]
1116 fn vaus_fire_button_on_bit3_independent_of_strobe() {
1117 let mut v = VausState::new();
1118 v.set(0x80, true);
1119 // No strobe yet; fire is returned directly regardless.
1120 assert_eq!(v.read() & (1 << 3), 1 << 3, "fire = bit 3 set");
1121 v.set(0x80, false);
1122 assert_eq!(v.read() & (1 << 3), 0, "fire released = bit 3 clear");
1123 }
1124
1125 #[test]
1126 fn vaus_knob_shifts_out_msb_first_inverted_on_bit4() {
1127 let mut v = VausState::new();
1128 // position 0b1010_0000: MSb-first raw bits = 1,0,1,0,0,0,0,0
1129 v.set(0b1010_0000, false);
1130 v.write_strobe(1);
1131 v.write_strobe(0);
1132 // Wire is inverted, so expected wire bits (bit 4) = 0,1,0,1,1,1,1,1.
1133 let expect_raw = [1u8, 0, 1, 0, 0, 0, 0, 0];
1134 for (i, raw) in expect_raw.iter().enumerate() {
1135 let byte = v.read();
1136 let wire_bit = (byte >> 4) & 1;
1137 assert_eq!(wire_bit, raw ^ 1, "read {i}: wire bit must be inverted raw");
1138 }
1139 }
1140
1141 #[test]
1142 fn vaus_post_empty_repeats_serial_in_bit() {
1143 let mut v = VausState::new();
1144 // LSb (serial-in) = 1 -> after the 8 real bits, reads repeat inverted 1 = 0.
1145 v.set(0b0000_0001, false);
1146 v.write_strobe(1);
1147 v.write_strobe(0);
1148 for _ in 0..8 {
1149 let _ = v.read();
1150 }
1151 // Now the register is all serial-in (1); wire bit = inverted = 0.
1152 for _ in 0..4 {
1153 assert_eq!((v.read() >> 4) & 1, 0);
1154 }
1155 }
1156
1157 #[test]
1158 fn zapper_light_detected_for_bright_pixel() {
1159 let mut z = ZapperState::new();
1160 z.set(10, 10, false);
1161 let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1162 // Bright white pixel at (10, 10).
1163 let idx = (10 * 256 + 10) * 4;
1164 fb[idx] = 0xFF;
1165 fb[idx + 1] = 0xFF;
1166 fb[idx + 2] = 0xFF;
1167 z.sample_light(&fb);
1168 // Light detected -> bit 3 = 0.
1169 assert_eq!(
1170 z.read() & (1 << 3),
1171 0,
1172 "bright pixel -> light detected (bit3=0)"
1173 );
1174 }
1175
1176 #[test]
1177 fn zapper_no_light_for_dark_pixel() {
1178 let mut z = ZapperState::new();
1179 z.set(10, 10, false);
1180 let fb = alloc::vec![0u8; 256 * 240 * 4]; // all black
1181 z.sample_light(&fb);
1182 assert_eq!(
1183 z.read() & (1 << 3),
1184 1 << 3,
1185 "dark pixel -> no light (bit3=1)"
1186 );
1187 }
1188
1189 #[test]
1190 fn zapper_off_screen_never_sees_light() {
1191 let mut z = ZapperState::new();
1192 z.set(1000, 1000, false);
1193 let mut fb = alloc::vec![0u8; 256 * 240 * 4];
1194 fb.fill(0xFF);
1195 z.sample_light(&fb);
1196 assert_eq!(z.read() & (1 << 3), 1 << 3, "off-screen aim -> no light");
1197 }
1198
1199 #[test]
1200 fn zapper_trigger_on_bit4() {
1201 let mut z = ZapperState::new();
1202 z.set(10, 10, true);
1203 assert_eq!(z.read() & (1 << 4), 1 << 4, "trigger pulled -> bit4 set");
1204 z.set(10, 10, false);
1205 assert_eq!(z.read() & (1 << 4), 0, "trigger released -> bit4 clear");
1206 }
1207
1208 /// Read 8 device bytes after a strobe, returning the bit-3 (L) and bit-4 (H)
1209 /// streams as bool arrays.
1210 fn powerpad_read8(p: &mut PowerPadState) -> ([bool; 8], [bool; 8]) {
1211 p.write_strobe(1);
1212 p.write_strobe(0);
1213 let mut l = [false; 8];
1214 let mut h = [false; 8];
1215 for i in 0..8 {
1216 let b = p.read();
1217 l[i] = b & (1 << 3) != 0;
1218 h[i] = b & (1 << 4) != 0;
1219 }
1220 (l, h)
1221 }
1222
1223 #[test]
1224 fn powerpad_no_buttons_reads_clear_then_h_ones() {
1225 // No buttons: L is all 0; H reads 0 for the first 4 (buttons 4,3,12,8),
1226 // then 1 for the trailing "read as H=1" bits.
1227 let mut p = PowerPadState::new();
1228 let (l, h) = powerpad_read8(&mut p);
1229 assert_eq!(l, [false; 8], "no L bits with nothing pressed");
1230 assert_eq!(h, [false, false, false, false, true, true, true, true]);
1231 }
1232
1233 #[test]
1234 fn powerpad_button_maps_to_expected_serial_position() {
1235 // Mat button "1" (index 0) is bit 1 of L -> appears on the 2nd read.
1236 let mut p = PowerPadState::new();
1237 p.set(1 << 0);
1238 let (l, _h) = powerpad_read8(&mut p);
1239 assert_eq!(l, [false, true, false, false, false, false, false, false]);
1240
1241 // Mat button "2" (index 1) is bit 0 of L -> appears on the 1st read.
1242 let mut p = PowerPadState::new();
1243 p.set(1 << 1);
1244 let (l, _h) = powerpad_read8(&mut p);
1245 assert_eq!(l, [true, false, false, false, false, false, false, false]);
1246
1247 // Mat button "4" (index 3) is bit 0 of H -> 1st read on bit 4.
1248 let mut p = PowerPadState::new();
1249 p.set(1 << 3);
1250 let (_l, h) = powerpad_read8(&mut p);
1251 assert_eq!(h, [true, false, false, false, true, true, true, true]);
1252 }
1253
1254 #[test]
1255 fn powerpad_strobe_high_reloads_each_read() {
1256 // While strobe is high, every read re-latches, so the first serial bit
1257 // is returned repeatedly (standard controller strobe semantics).
1258 let mut p = PowerPadState::new();
1259 p.set(1 << 1); // button "2" -> L bit 0 (1st-read position).
1260 p.write_strobe(1); // strobe held high
1261 for _ in 0..5 {
1262 assert_eq!(p.read() & (1 << 3), 1 << 3, "strobe-high repeats bit 0");
1263 }
1264 }
1265
1266 #[test]
1267 fn powerpad_save_state_round_trip() {
1268 let mut p = PowerPadState::new();
1269 p.set(0b1010_0101_0011);
1270 p.write_strobe(1);
1271 p.write_strobe(0);
1272 let _ = p.read(); // advance the registers
1273 let restored = PowerPadState::from_parts(
1274 p.buttons_raw(),
1275 p.shift_l_raw(),
1276 p.shift_h_raw(),
1277 p.strobe_raw(),
1278 );
1279 assert_eq!(restored.peek(), p.peek());
1280 assert_eq!(restored.buttons_raw(), 0b1010_0101_0011);
1281 }
1282
1283 #[test]
1284 fn powerpad_masks_to_12_bits() {
1285 // Bits above 11 are ignored (the mat has 12 buttons).
1286 let mut p = PowerPadState::new();
1287 p.set(0xFFFF);
1288 assert_eq!(p.buttons_raw(), 0x0FFF);
1289 }
1290
1291 #[test]
1292 fn input_device_enum_dispatch() {
1293 let mut d = InputDevice::Vaus(VausState::new());
1294 d.write_strobe(1);
1295 d.write_strobe(0);
1296 let _ = d.read();
1297 let mut z = InputDevice::Zapper(ZapperState::new());
1298 // Strobe is a no-op for the Zapper.
1299 z.write_strobe(1);
1300 assert_eq!(z.read() & (1 << 3), 1 << 3, "zapper: no light by default");
1301 }
1302
1303 /// Shift out `n` D0 bits (MSb-first), returning them packed into a u64 in
1304 /// read order (first bit = most significant of the returned `n`-bit value).
1305 fn mouse_read_bits(m: &mut SnesMouseState, n: usize) -> u64 {
1306 m.write_strobe(1);
1307 m.write_strobe(0);
1308 let mut acc = 0u64;
1309 for _ in 0..n {
1310 acc = (acc << 1) | u64::from(m.read() & 1);
1311 }
1312 acc
1313 }
1314
1315 #[test]
1316 fn snes_mouse_signature_nibble_is_0b0001() {
1317 let mut m = SnesMouseState::new();
1318 // First 4 bits are the device-id signature nibble 0b0001.
1319 let bits = mouse_read_bits(&mut m, 4);
1320 assert_eq!(bits, 0b0001, "signature nibble must be 0b0001");
1321 }
1322
1323 #[test]
1324 fn snes_mouse_full_report_encodes_buttons_and_movement() {
1325 let mut m = SnesMouseState::new();
1326 // dx = +5, dy = -3, left pressed, sensitivity = 2 (high).
1327 m.set(5, -3, true, false, 2);
1328 let word = mouse_read_bits(&mut m, 32);
1329 // Reconstruct the full 32-bit word and check each field.
1330 assert_eq!((word >> 28) & 0x0F, 0b0001, "signature");
1331 assert_eq!((word >> 24) & 0b11, 2, "sensitivity");
1332 assert_eq!((word >> 23) & 1, 1, "left button");
1333 assert_eq!((word >> 22) & 1, 0, "right button");
1334 // Y field (bits 15..8): sign=1 (negative), magnitude 3.
1335 let y = (word >> 8) & 0xFF;
1336 assert_eq!((y >> 7) & 1, 1, "Y sign negative");
1337 assert_eq!(y & 0x7F, 3, "Y magnitude");
1338 // X field (bits 7..0): sign=0 (positive), magnitude 5.
1339 let x = word & 0xFF;
1340 assert_eq!((x >> 7) & 1, 0, "X sign positive");
1341 assert_eq!(x & 0x7F, 5, "X magnitude");
1342 }
1343
1344 #[test]
1345 fn snes_mouse_idles_high_after_32_bits() {
1346 let mut m = SnesMouseState::new();
1347 m.write_strobe(1);
1348 m.write_strobe(0);
1349 for _ in 0..32 {
1350 let _ = m.read();
1351 }
1352 for _ in 0..4 {
1353 assert_eq!(m.read() & 1, 1, "serial line idles high after the report");
1354 }
1355 }
1356
1357 #[test]
1358 fn snes_mouse_clamps_movement_to_127() {
1359 let mut m = SnesMouseState::new();
1360 m.set(1000, -1000, false, false, 0);
1361 let word = mouse_read_bits(&mut m, 32);
1362 assert_eq!(word & 0x7F, 127, "X magnitude clamps to 127");
1363 assert_eq!((word >> 8) & 0x7F, 127, "Y magnitude clamps to 127");
1364 }
1365
1366 #[test]
1367 fn snes_mouse_enc_axis_handles_i16_extremes_without_panic() {
1368 // Regression: `enc_axis(i16::MIN)` must not panic on the `-v` overflow
1369 // (`-(-32768)` is unrepresentable as i16). Both extremes encode sanely:
1370 // sign bit set/clear and magnitude clamped to the 7-bit max of 127.
1371 let mut m = SnesMouseState::new();
1372 m.set(i16::MIN, i16::MAX, false, false, 0);
1373 let word = mouse_read_bits(&mut m, 32);
1374 // X = i16::MIN: negative -> sign bit (bit 7) set, magnitude clamped 127.
1375 assert_eq!(word & 0x80, 0x80, "i16::MIN encodes as negative");
1376 assert_eq!(word & 0x7F, 127, "i16::MIN magnitude clamps to 127");
1377 // Y = i16::MAX: positive -> sign bit clear, magnitude clamped 127.
1378 assert_eq!((word >> 8) & 0x80, 0, "i16::MAX encodes as positive");
1379 assert_eq!((word >> 8) & 0x7F, 127, "i16::MAX magnitude clamps to 127");
1380 }
1381
1382 #[test]
1383 fn snes_mouse_strobe_high_repeats_signature_bit() {
1384 let mut m = SnesMouseState::new();
1385 m.write_strobe(1); // held high
1386 for _ in 0..5 {
1387 // Signature MSb (bit 31 of 0b0001 << 28) is 0; repeats while strobed.
1388 assert_eq!(m.read() & 1, 0, "strobe-high repeats bit 31");
1389 }
1390 }
1391
1392 #[test]
1393 fn family_keyboard_disabled_reads_all_high() {
1394 let k = FamilyKeyboardState::new();
1395 // Not enabled (bit 2 unset): key switches all read high (bits 4..1 set).
1396 assert_eq!(
1397 k.read(),
1398 0b0001_1110,
1399 "disabled matrix -> no keys (bits 4..1=1)"
1400 );
1401 }
1402
1403 #[test]
1404 fn family_keyboard_pressed_key_reads_active_low() {
1405 let mut k = FamilyKeyboardState::new();
1406 // Press key at row 0, column-half 0, switch 0 (linear index 0).
1407 k.set_key(0, true);
1408 // Enable matrix (bit2), select column-half 0 (bit0=0), clock low resets row to 0.
1409 k.write_strobe(0b0000_0100);
1410 let r = k.read();
1411 // The pressed switch (bit 0 of the nibble) appears active-low on bit 1.
1412 assert_eq!(r & (1 << 1), 0, "pressed key reads 0 (active-low) on bit 1");
1413 // The other three switches are not pressed -> read 1.
1414 assert_eq!(r & (1 << 2), 1 << 2);
1415 assert_eq!(r & (1 << 3), 1 << 3);
1416 assert_eq!(r & (1 << 4), 1 << 4);
1417 }
1418
1419 #[test]
1420 fn family_keyboard_column_select_picks_high_nibble() {
1421 let mut k = FamilyKeyboardState::new();
1422 // Key at row 0, column-half 1, switch 0 = linear index 4 (row*8 + 4).
1423 k.set_key(4, true);
1424 // Enable + select column-half 1 (bit0=1), clock low (resets row to 0).
1425 k.write_strobe(0b0000_0101);
1426 let r = k.read();
1427 assert_eq!(
1428 r & (1 << 1),
1429 0,
1430 "column-half-1 key reads active-low on bit 1"
1431 );
1432 // Selecting column-half 0 instead shows nothing pressed there.
1433 k.write_strobe(0b0000_0100);
1434 assert_eq!(k.read() & (1 << 1), 1 << 1, "column-half 0 has no key here");
1435 }
1436
1437 #[test]
1438 fn family_keyboard_clock_edge_advances_row() {
1439 let mut k = FamilyKeyboardState::new();
1440 // Press a key on row 1, column-half 0, switch 0 = linear index 8.
1441 k.set_key(8, true);
1442 // Enable + clock low -> row 0.
1443 k.write_strobe(0b0000_0100);
1444 assert_eq!(k.read() & (1 << 1), 1 << 1, "row 0 has no key");
1445 // Rising clock edge (bit1 0->1) advances to row 1.
1446 k.write_strobe(0b0000_0110);
1447 assert_eq!(
1448 k.read() & (1 << 1),
1449 0,
1450 "row 1 key now selected (active-low)"
1451 );
1452 }
1453
1454 #[test]
1455 fn family_keyboard_save_state_round_trip() {
1456 let mut k = FamilyKeyboardState::new();
1457 k.set_key(8, true);
1458 k.set_key(40, true);
1459 k.write_strobe(0b0000_0110);
1460 let restored = FamilyKeyboardState::from_parts(
1461 k.keys_raw(),
1462 k.row_raw(),
1463 k.column_raw(),
1464 k.enabled_raw(),
1465 k.clock_raw(),
1466 );
1467 assert_eq!(restored.read(), k.read());
1468 assert_eq!(restored.keys_raw(), k.keys_raw());
1469 }
1470
1471 #[test]
1472 fn family_keyboard_from_parts_clamps_out_of_range_row() {
1473 // A corrupt/malicious save-state must not be able to drive a row value
1474 // that would index `self.keys[row]` out of bounds in `read()`.
1475 let keys = [0u8; FAMILY_KEYBOARD_ROWS];
1476 let restored = FamilyKeyboardState::from_parts(keys, 250, false, true, false);
1477 assert!(
1478 (restored.row_raw() as usize) < FAMILY_KEYBOARD_ROWS,
1479 "out-of-range row saturated to the matrix bound"
1480 );
1481 // Must not panic: enabled matrix indexes keys[row] in read().
1482 let _ = restored.read();
1483 }
1484
1485 #[test]
1486 fn family_keyboard_set_key_out_of_range_is_noop() {
1487 let mut k = FamilyKeyboardState::new();
1488 k.set_key(FAMILY_KEYBOARD_KEYS, true); // index == 72, out of range
1489 k.set_key(1000, true);
1490 assert_eq!(k.keys_raw(), [0; FAMILY_KEYBOARD_ROWS]);
1491 }
1492
1493 // --- v1.3.0 Workstream F1 — niche peripheral aliases + Hyper Shots ---
1494
1495 #[test]
1496 fn family_trainer_reuses_power_pad_scan() {
1497 // The Family Trainer is layout-equivalent to the Power Pad: an identical
1498 // PowerPadState must produce an identical serial readout through both
1499 // InputDevice variants.
1500 let mut pad = InputDevice::PowerPad(PowerPadState::new());
1501 let mut mat = InputDevice::FamilyTrainer(PowerPadState::new());
1502 if let (InputDevice::PowerPad(p), InputDevice::FamilyTrainer(m)) = (&mut pad, &mut mat) {
1503 p.set(0b1010_0101_0011);
1504 m.set(0b1010_0101_0011);
1505 }
1506 pad.write_strobe(1);
1507 pad.write_strobe(0);
1508 mat.write_strobe(1);
1509 mat.write_strobe(0);
1510 for i in 0..8 {
1511 assert_eq!(pad.read(), mat.read(), "read {i}: trainer == power pad");
1512 }
1513 }
1514
1515 #[test]
1516 fn subor_keyboard_reuses_family_keyboard_scan() {
1517 // The Subor keyboard reuses the Family BASIC keyboard matrix scan; the
1518 // same key state must read identically through both variants.
1519 let mut fam = FamilyKeyboardState::new();
1520 let mut sub = FamilyKeyboardState::new();
1521 fam.set_key(8, true);
1522 sub.set_key(8, true);
1523 let mut famd = InputDevice::FamilyKeyboard(fam);
1524 let mut subd = InputDevice::SuborKeyboard(sub);
1525 // Enable + clock low (row 0), then rising edge -> row 1.
1526 for v in [0b0000_0100u8, 0b0000_0110] {
1527 famd.write_strobe(v);
1528 subd.write_strobe(v);
1529 }
1530 assert_eq!(famd.read(), subd.read(), "subor == family keyboard read");
1531 assert_eq!(famd.peek(), subd.peek());
1532 }
1533
1534 #[test]
1535 fn konami_hyper_shot_buttons_on_expected_bits() {
1536 let mut h = KonamiHyperShotState::new();
1537 // P1 Run (bit0) -> read bit 1; P2 Jump (bit3) -> read bit 4.
1538 h.set(0b1001);
1539 h.write_strobe(0); // enable both players (active-low)
1540 let r = h.read();
1541 assert_eq!(r & (1 << 1), 1 << 1, "P1 Run on bit 1");
1542 assert_eq!(r & (1 << 4), 1 << 4, "P2 Jump on bit 4");
1543 assert_eq!(r & (1 << 2), 0, "P1 Jump not pressed");
1544 assert_eq!(r & (1 << 3), 0, "P2 Run not pressed");
1545 }
1546
1547 #[test]
1548 fn konami_hyper_shot_disabled_player_reads_zero() {
1549 let mut h = KonamiHyperShotState::new();
1550 h.set(0b1111); // all four buttons held
1551 // Disable P1 (bit 1 set), enable P2 (bit 2 clear).
1552 h.write_strobe(0b0000_0010);
1553 let r = h.read();
1554 assert_eq!(r & (1 << 1), 0, "disabled P1 Run reads 0");
1555 assert_eq!(r & (1 << 2), 0, "disabled P1 Jump reads 0");
1556 assert_eq!(r & (1 << 3), 1 << 3, "enabled P2 Run reads pressed");
1557 assert_eq!(r & (1 << 4), 1 << 4, "enabled P2 Jump reads pressed");
1558 }
1559
1560 #[test]
1561 fn konami_hyper_shot_save_state_round_trip() {
1562 let mut h = KonamiHyperShotState::new();
1563 h.set(0b0110);
1564 h.write_strobe(0b0000_0100); // disable P2
1565 let r = KonamiHyperShotState::from_parts(
1566 h.buttons_raw(),
1567 h.p1_enabled_raw(),
1568 h.p2_enabled_raw(),
1569 );
1570 assert_eq!(r.peek(), h.peek());
1571 assert_eq!(r.buttons_raw(), 0b0110);
1572 }
1573
1574 #[test]
1575 fn bandai_hyper_shot_select_picks_sensor_group() {
1576 let mut b = BandaiHyperShotState::new();
1577 // Group A=0 = bits 0..=3 (Left Hook = bit 0), A=1 = bits 4..=7.
1578 b.set(0b0001_0001); // Left Hook (group0) + Left Jab (group1)
1579 b.write_strobe(0); // A=0 group
1580 assert_eq!(b.read() & (1 << 1), 1 << 1, "group0 sensor 0 on bit 1");
1581 b.write_strobe(0b0000_0010); // A=1 group
1582 assert_eq!(b.read() & (1 << 1), 1 << 1, "group1 sensor 0 on bit 1");
1583 // A sensor only in group0 must vanish when the A=1 group is selected.
1584 let mut b2 = BandaiHyperShotState::new();
1585 b2.set(0b0000_1000); // only group0 bit 3 (Right Hook)
1586 b2.write_strobe(0b0000_0010); // select A=1
1587 assert_eq!(b2.read() & 0b1_1110, 0, "group0-only sensor absent in A=1");
1588 }
1589
1590 #[test]
1591 fn bandai_hyper_shot_save_state_round_trip() {
1592 let mut b = BandaiHyperShotState::new();
1593 b.set(0b1100_0011);
1594 b.write_strobe(0b0000_0010);
1595 let r = BandaiHyperShotState::from_parts(b.sensors_raw(), b.select_raw());
1596 assert_eq!(r.peek(), b.peek());
1597 assert_eq!(r.sensors_raw(), 0b1100_0011);
1598 assert!(r.select_raw());
1599 }
1600
1601 #[test]
1602 fn hyper_shots_dispatch_through_input_device() {
1603 let mut k = InputDevice::KonamiHyperShot(KonamiHyperShotState::new());
1604 k.write_strobe(0);
1605 let _ = k.read();
1606 let _ = k.peek();
1607 let mut bd = InputDevice::BandaiHyperShot(BandaiHyperShotState::new());
1608 bd.write_strobe(0);
1609 let _ = bd.read();
1610 let _ = bd.peek();
1611 }
1612}