Skip to main content

rustysnes_core/
controller.rs

1//! SNES controller-port peripherals beyond the standard gamepad: Mouse, Super Scope, and Super
2//! Multitap (`v0.9.0`, Phase 7's "niche peripherals" exit criterion).
3//!
4//! Ported from ares' `sfc/controller/{mouse,super-scope,super-multitap}` — real hardware's
5//! 2-bit-per-clock (`data1`/`data2`) serial-shift-register protocol per controller port, selected
6//! via [`PortDevice`]. [`PortDevice::Gamepad`] (the default, both ports) is this project's
7//! original, unchanged single-bit 16-bit-shift-register model (`crate::Bus`'s own `joypad`
8//! field) — every other device is opt-in, selected explicitly via [`crate::Bus::set_port_device`],
9//! and touches no code on the default path.
10//!
11//! Each device here owns exactly the same two operations real hardware's controller-port pin 2
12//! (clock/latch) and pins 4-5 (`data1`/`data2`) expose: `latch(strobe)` (the `$4016` bit-0 write,
13//! wired to BOTH ports simultaneously on real hardware — there is only one physical strobe line)
14//! reloads/repacks the device's shift register from its latest host-supplied input; `clock()`
15//! shifts one bit (or, for `MultitapState`, one bit from each of two sub-pads at once) out MSB
16//! first, refilling with `1` past the real bit count — matching every real SNES serial peripheral's
17//! floating/pulled-high behavior once its shift register empties.
18
19use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
20
21/// Which peripheral occupies a controller port (`0` = port 1, `1` = port 2).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum PortDevice {
24    /// The original standard SNES pad — [`crate::Bus`]'s own 16-bit `joypad` shift register,
25    /// unchanged by this module.
26    #[default]
27    Gamepad,
28    /// SNES Mouse — a 32-bit `data1`-only shift register (relative X/Y + buttons + speed).
29    Mouse,
30    /// Super Scope light gun — an 8-bit `data1`-only shift register, plus a PPU H/V-counter-latch
31    /// side channel driven from `crate::Bus::advance_master` (real hardware: wired only to
32    /// controller port 2's IOBIT pin — a Super Scope in port 1 never receives a beam-position
33    /// latch, matching ares' own documented hardware note).
34    SuperScope,
35    /// Super Multitap — four independent standard-pad sub-ports, `data1`/`data2` both live
36    /// simultaneously, the currently-addressed pair (`[0,1]` vs `[2,3]`) selected by the shared
37    /// IOBIT pin.
38    Multitap,
39}
40
41impl PortDevice {
42    const fn to_tag(self) -> u8 {
43        match self {
44            Self::Gamepad => 0,
45            Self::Mouse => 1,
46            Self::SuperScope => 2,
47            Self::Multitap => 3,
48        }
49    }
50
51    const fn from_tag(tag: u8) -> Self {
52        match tag {
53            1 => Self::Mouse,
54            2 => Self::SuperScope,
55            3 => Self::Multitap,
56            _ => Self::Gamepad,
57        }
58    }
59}
60
61/// SNES Mouse peripheral state (ares `Controller::Mouse`).
62#[derive(Debug, Clone, Copy, Default)]
63pub(crate) struct MouseState {
64    /// Host-supplied relative delta pending the next latch, set once per frame by
65    /// [`crate::Bus::set_mouse`] — mirrors [`crate::Bus::set_joypad`]'s "always replace,
66    /// re-synced once per frame" convention (unscaled/unclamped host units; the speed multiplier
67    /// and 127-magnitude clamp below are applied at latch time, matching real hardware).
68    pending_dx: i32,
69    pending_dy: i32,
70    pending_left: bool,
71    pending_right: bool,
72    /// 0 = slow (1.0x), 1 = normal (1.5x), 2 = fast (2.0x). Real hardware quirk: this cycles on
73    /// every read taken *while strobe is held high*, not via any CPU register write — modeled in
74    /// [`Self::clock`].
75    speed: u8,
76    /// The packed 32-bit shift register, computed at [`Self::latch`] time.
77    shift: u32,
78    /// Last-observed strobe level; latching is a no-op unless it actually changes (ares'
79    /// `if(latched==data) return;`).
80    latched: bool,
81}
82
83impl MouseState {
84    pub(crate) const fn set_input(&mut self, dx: i32, dy: i32, left: bool, right: bool) {
85        self.pending_dx = dx;
86        self.pending_dy = dy;
87        self.pending_left = left;
88        self.pending_right = right;
89    }
90
91    /// `$4016`/`$4017` bit-0 write. Real hardware re-samples on *every* transition (ares' own
92    /// `latch()` doesn't distinguish direction), so this does too.
93    #[allow(clippy::similar_names)] // `neg_x`/`neg_y` are the real, distinct hardware direction bits.
94    pub(crate) fn latch(&mut self, strobe: bool) {
95        if self.latched == strobe {
96            return;
97        }
98        self.latched = strobe;
99        let neg_x = u32::from(self.pending_dx < 0);
100        let neg_y = u32::from(self.pending_dy < 0);
101        let multiplier: f64 = match self.speed {
102            1 => 1.5,
103            2 => 2.0,
104            _ => 1.0,
105        };
106        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
107        let cx = (f64::from(self.pending_dx.unsigned_abs()) * multiplier).min(127.0) as u32;
108        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
109        let cy = (f64::from(self.pending_dy.unsigned_abs()) * multiplier).min(127.0) as u32;
110        self.shift = (u32::from(self.pending_right) << 23)
111            | (u32::from(self.pending_left) << 22)
112            | (u32::from((self.speed >> 1) & 1) << 21)
113            | (u32::from(self.speed & 1) << 20)
114            | (1 << 16) // device signature 0,0,0,1 (bits 19..16)
115            | (neg_y << 15)
116            | ((cy & 0x7F) << 8)
117            | (neg_x << 7)
118            | (cx & 0x7F);
119    }
120
121    /// `$4016`/`$4017` clock (one bit per read).
122    pub(crate) const fn clock(&mut self) -> u8 {
123        if self.latched {
124            self.speed = (self.speed + 1) % 3;
125            return 0;
126        }
127        let bit = ((self.shift >> 31) & 1) as u8;
128        self.shift = (self.shift << 1) | 1;
129        bit
130    }
131
132    fn save_state(self, s: &mut SaveWriter) {
133        s.write_u8(self.speed);
134        s.write_u32(self.shift);
135        s.write_bool(self.latched);
136    }
137
138    fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
139        Ok(Self {
140            speed: s.read_u8()?,
141            shift: s.read_u32()?,
142            latched: s.read_bool()?,
143            ..Self::default()
144        })
145    }
146}
147
148/// Super Scope button bitmask constants for [`crate::Bus::set_superscope`] — real, independent
149/// physical switches, not mutually-exclusive states, hence a bitmask rather than an enum.
150pub mod scope {
151    /// The trigger.
152    pub const TRIGGER: u8 = 1 << 0;
153    /// The cursor button (a secondary "select" button on the gun's body).
154    pub const CURSOR: u8 = 1 << 1;
155    /// The Turbo switch — toggles the trigger into level-sensitive (rapid-fire) mode.
156    pub const TURBO: u8 = 1 << 2;
157    /// The Pause button.
158    pub const PAUSE: u8 = 1 << 3;
159}
160
161/// Super Scope light-gun peripheral state (ares `Controller::SuperScope`). Genuinely needs this
162/// many independent booleans — each is a distinct real hardware latch/edge-detector, not an API
163/// design choice (`crate::controller::scope`'s public bitmask keeps the *external* API to one
164/// packed byte instead).
165#[allow(clippy::struct_excessive_bools)]
166#[derive(Debug, Clone, Copy, Default)]
167pub(crate) struct SuperScopeState {
168    /// Host-supplied absolute screen-space target. Clamped to ares' own `-16..=256+16`/
169    /// `-16..=240+16` fringe (the small negative/over-max margin is what makes off-screen
170    /// detection possible — a hard `0..255` clamp would make "aim off-screen" unrepresentable).
171    x: i32,
172    y: i32,
173    /// Live physical button/switch state, a `scope::{TRIGGER,CURSOR,TURBO,PAUSE}` bitmask.
174    buttons: u8,
175
176    latched: bool,
177    shift: u16,
178    /// Set on every latch transition (ares' `counter=0`), cleared and acted on the next time
179    /// [`Self::clock`] is called — the button/edge sampling must happen exactly ONCE per read
180    /// pass (ares' `if(counter==0)` inside `data()`, not inside `latch()`), since a real read
181    /// pass is always a strobe-HIGH write followed by a strobe-LOW write before any clocking
182    /// happens; sampling inside `latch()` itself would run the (stateful) edge-detectors twice —
183    /// once per transition — corrupting `trigger_lock`/`turbo_prev`/`pause_lock`.
184    pending_sample: bool,
185
186    turbo_edge: bool,
187    turbo_prev: bool,
188    trigger_lock: bool,
189    pause_edge: bool,
190    pause_lock: bool,
191}
192
193impl SuperScopeState {
194    pub(crate) fn set_input(&mut self, x: i32, y: i32, buttons: u8) {
195        self.x = x.clamp(-16, 256 + 16);
196        self.y = y.clamp(-16, 240 + 16);
197        self.buttons = buttons;
198    }
199
200    fn offscreen(&self, visible_height: u16) -> bool {
201        self.x < 0 || self.y < 0 || self.x >= 256 || self.y >= i32::from(visible_height)
202    }
203
204    /// `$4016`/`$4017` bit-0 write.
205    pub(crate) const fn latch(&mut self, strobe: bool) {
206        if self.latched == strobe {
207            return;
208        }
209        self.latched = strobe;
210        self.pending_sample = true;
211    }
212
213    /// The button/edge sample this read pass will use, computed once (see [`Self::pending_sample`]'s
214    /// doc) then packed into [`Self::shift`].
215    fn sample(&mut self, visible_height: u16) {
216        self.pending_sample = false;
217
218        let turbo = self.buttons & scope::TURBO != 0;
219        if turbo && !self.turbo_prev {
220            self.turbo_edge = !self.turbo_edge;
221        }
222        self.turbo_prev = turbo;
223
224        let trigger = self.buttons & scope::TRIGGER != 0;
225        let mut trigger_value = false;
226        if trigger && (self.turbo_edge || !self.trigger_lock) {
227            trigger_value = true;
228            self.trigger_lock = true;
229        } else if !trigger {
230            self.trigger_lock = false;
231        }
232
233        let pause = self.buttons & scope::PAUSE != 0;
234        self.pause_edge = false;
235        if pause && !self.pause_lock {
236            self.pause_edge = true;
237            self.pause_lock = true;
238        } else if !pause {
239            self.pause_lock = false;
240        }
241
242        let cursor = self.buttons & scope::CURSOR != 0;
243        let offscreen = self.offscreen(visible_height);
244        self.shift = (u16::from(trigger_value && !offscreen) << 15)
245            | (u16::from(cursor) << 14)
246            | (u16::from(self.turbo_edge) << 13)
247            | (u16::from(self.pause_edge) << 12)
248            | (u16::from(offscreen) << 9)
249            | 0x00FF; // bits 7..0 pre-filled `1`, matching real hardware's post-bit-8 free-run.
250    }
251
252    pub(crate) fn clock(&mut self, visible_height: u16) -> u8 {
253        if self.pending_sample {
254            self.sample(visible_height);
255        }
256        let bit = ((self.shift >> 15) & 1) as u8;
257        self.shift = (self.shift << 1) | 1;
258        bit
259    }
260
261    /// The beam position (in this project's dot-space, `dot = master_clock / 4`) this Super Scope
262    /// is "aimed at", for [`crate::Bus`]'s per-master-clock H/V-counter auto-latch check — `None`
263    /// while aimed off-screen (real hardware: the light sensor simply never fires,
264    /// `if(!offscreen) { ... }`).
265    pub(crate) fn beam_target(&self, visible_height: u16) -> Option<(u16, u16)> {
266        if self.offscreen(visible_height) {
267            return None;
268        }
269        // +24 dots is the light sensor's own fixed detection delay (ares' `(cx+24)*4` hcounter
270        // offset — already dot-space here since our `dot()` is hcounter/4, so the `*4`/`/4`
271        // cancel).
272        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
273        Some((self.y as u16, (self.x + 24) as u16))
274    }
275
276    fn save_state(self, s: &mut SaveWriter) {
277        s.write_bool(self.latched);
278        s.write_u16(self.shift);
279        s.write_bool(self.turbo_edge);
280        s.write_bool(self.turbo_prev);
281        s.write_bool(self.trigger_lock);
282        s.write_bool(self.pause_edge);
283        s.write_bool(self.pause_lock);
284    }
285
286    fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
287        Ok(Self {
288            latched: s.read_bool()?,
289            shift: s.read_u16()?,
290            turbo_edge: s.read_bool()?,
291            turbo_prev: s.read_bool()?,
292            trigger_lock: s.read_bool()?,
293            pause_edge: s.read_bool()?,
294            pause_lock: s.read_bool()?,
295            ..Self::default()
296        })
297    }
298}
299
300/// Super Multitap peripheral state (ares `Controller::SuperMultitap`) — four independent
301/// standard-pad sub-ports on one physical port, `data1`/`data2` both live simultaneously, the
302/// currently-addressed pair selected by the shared IOBIT pin (real hardware: `[0,1]` vs `[2,3]`).
303#[derive(Debug, Clone, Copy, Default)]
304pub(crate) struct MultitapState {
305    /// Each sub-pad's own 16-bit shift register — the exact `crate::Bus::joypad` model, one per
306    /// virtual player.
307    pads: [u16; 4],
308    latched: bool,
309}
310
311impl MultitapState {
312    /// Reload sub-pad `index`'s (`0..=3`) shift register from a fresh 16-bit button snapshot
313    /// (`BYsSUDLR....`, matching [`crate::Bus::set_joypad`]'s own format/convention exactly).
314    pub(crate) fn set_pad(&mut self, index: usize, buttons: u16) {
315        if let Some(pad) = self.pads.get_mut(index) {
316            *pad = buttons;
317        }
318    }
319
320    pub(crate) fn pad(&self, index: usize) -> u16 {
321        self.pads.get(index).copied().unwrap_or(0)
322    }
323
324    /// `$4016`/`$4017` bit-0 write.
325    pub(crate) const fn latch(&mut self, strobe: bool) {
326        self.latched = strobe;
327    }
328
329    /// `$4016`/`$4017` clock — returns `(data1, data2)`. While latched, both lines report the
330    /// fixed Super Multitap device-detection signature (`data1=0, data2=1`); otherwise the
331    /// IOBIT-selected pair's sub-pads each shift one bit (only the SELECTED pair's counters
332    /// advance — the other pair's registers stay untouched until they're the ones selected,
333    /// matching real hardware exactly).
334    pub(crate) const fn clock(&mut self, iobit: bool) -> (u8, u8) {
335        if self.latched {
336            return (0, 1);
337        }
338        let (a, b) = if iobit { (0, 1) } else { (2, 3) };
339        let bit_a = ((self.pads[a] >> 15) & 1) as u8;
340        self.pads[a] = (self.pads[a] << 1) | 1;
341        let bit_b = ((self.pads[b] >> 15) & 1) as u8;
342        self.pads[b] = (self.pads[b] << 1) | 1;
343        (bit_a, bit_b)
344    }
345
346    fn save_state(self, s: &mut SaveWriter) {
347        for pad in self.pads {
348            s.write_u16(pad);
349        }
350        s.write_bool(self.latched);
351    }
352
353    fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
354        let mut pads = [0u16; 4];
355        for pad in &mut pads {
356            *pad = s.read_u16()?;
357        }
358        Ok(Self {
359            pads,
360            latched: s.read_bool()?,
361        })
362    }
363}
364
365/// One controller port's full peripheral state — which [`PortDevice`] is attached, plus that
366/// device's own runtime state (the others stay at their default/idle value, costing only the
367/// stack space of the largest variant; no allocation).
368#[derive(Debug, Clone, Copy, Default)]
369pub(crate) struct PortState {
370    pub(crate) device: PortDevice,
371    pub(crate) mouse: MouseState,
372    pub(crate) super_scope: SuperScopeState,
373    pub(crate) multitap: MultitapState,
374}
375
376impl PortState {
377    pub(crate) fn latch(&mut self, strobe: bool) {
378        match self.device {
379            PortDevice::Gamepad => {}
380            PortDevice::Mouse => self.mouse.latch(strobe),
381            PortDevice::SuperScope => self.super_scope.latch(strobe),
382            PortDevice::Multitap => self.multitap.latch(strobe),
383        }
384    }
385
386    /// Returns `(data1, data2)` for this port's device — `Gamepad` is handled by the caller
387    /// (`crate::Bus` keeps its own pre-existing `joypad` field/logic untouched), so this only
388    /// covers the three peripherals added here.
389    pub(crate) fn clock(&mut self, iobit: bool, visible_height: u16) -> (u8, u8) {
390        match self.device {
391            PortDevice::Gamepad => (0, 0),
392            PortDevice::Mouse => (self.mouse.clock(), 0),
393            PortDevice::SuperScope => (self.super_scope.clock(visible_height), 0),
394            PortDevice::Multitap => self.multitap.clock(iobit),
395        }
396    }
397
398    pub(crate) fn save_state(self, s: &mut SaveWriter) {
399        s.write_u8(self.device.to_tag());
400        self.mouse.save_state(s);
401        self.super_scope.save_state(s);
402        self.multitap.save_state(s);
403    }
404
405    pub(crate) fn load_state(s: &mut SaveReader) -> Result<Self, SaveStateError> {
406        let device = PortDevice::from_tag(s.read_u8()?);
407        let mouse = MouseState::load_state(s)?;
408        let super_scope = SuperScopeState::load_state(s)?;
409        let multitap = MultitapState::load_state(s)?;
410        Ok(Self {
411            device,
412            mouse,
413            super_scope,
414            multitap,
415        })
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use alloc::vec::Vec;
422
423    use super::{MouseState, MultitapState, PortDevice, SuperScopeState, scope};
424
425    /// Shift out `n` bits MSB-first, as a real CPU polling `$4016`/`$4017` `n` times would.
426    fn shift_bits<T>(mut clock: impl FnMut(&mut T) -> u8, state: &mut T, n: usize) -> Vec<u8> {
427        (0..n).map(|_| clock(state)).collect()
428    }
429
430    #[test]
431    fn port_device_tag_round_trips() {
432        for device in [
433            PortDevice::Gamepad,
434            PortDevice::Mouse,
435            PortDevice::SuperScope,
436            PortDevice::Multitap,
437        ] {
438            assert_eq!(PortDevice::from_tag(device.to_tag()), device);
439        }
440    }
441
442    #[test]
443    fn mouse_signature_and_button_bits() {
444        let mut mouse = MouseState::default();
445        // No motion, both buttons held, speed stays at its default (0 = slow).
446        mouse.set_input(0, 0, true, true);
447        mouse.latch(true); // strobe high: arm.
448        mouse.latch(false); // strobe low: pack + begin shifting.
449        let bits = shift_bits(MouseState::clock, &mut mouse, 32);
450        // bits[0..8]  = 0 (padding)
451        assert!(bits[0..8].iter().all(|&b| b == 0));
452        // bits[8]=right, bits[9]=left — both held.
453        assert_eq!(bits[8], 1, "right button bit");
454        assert_eq!(bits[9], 1, "left button bit");
455        // bits[10..12] = speed (0,0 = slow, the default).
456        assert_eq!(bits[10], 0);
457        assert_eq!(bits[11], 0);
458        // bits[12..16] = device signature 0,0,0,1.
459        assert_eq!(&bits[12..16], &[0, 0, 0, 1], "mouse device signature");
460        // No motion: direction bits + magnitude all zero.
461        assert_eq!(bits[16], 0, "dy direction");
462        assert!(bits[17..24].iter().all(|&b| b == 0), "cy magnitude");
463        assert_eq!(bits[24], 0, "dx direction");
464        assert!(bits[25..32].iter().all(|&b| b == 0), "cx magnitude");
465    }
466
467    #[test]
468    fn mouse_direction_and_magnitude() {
469        let mut mouse = MouseState::default();
470        mouse.set_input(-5, 10, false, false);
471        mouse.latch(true);
472        mouse.latch(false);
473        let bits = shift_bits(MouseState::clock, &mut mouse, 32);
474        assert_eq!(
475            bits[16], 0,
476            "moving down (positive dy) clears the dy-direction bit"
477        );
478        let cy = bits[17..24]
479            .iter()
480            .fold(0u32, |acc, &b| (acc << 1) | u32::from(b));
481        assert_eq!(cy, 10, "cy magnitude, no speed multiplier at default speed");
482        assert_eq!(
483            bits[24], 1,
484            "moving left (negative dx) sets the dx-direction bit"
485        );
486        let cx = bits[25..32]
487            .iter()
488            .fold(0u32, |acc, &b| (acc << 1) | u32::from(b));
489        assert_eq!(cx, 5, "cx magnitude");
490    }
491
492    #[test]
493    fn mouse_magnitude_clamps_to_127() {
494        let mut mouse = MouseState::default();
495        mouse.set_input(500, 0, false, false);
496        mouse.latch(true);
497        mouse.latch(false);
498        let bits = shift_bits(MouseState::clock, &mut mouse, 32);
499        let cx = bits[25..32]
500            .iter()
501            .fold(0u32, |acc, &b| (acc << 1) | u32::from(b));
502        assert_eq!(
503            cx, 127,
504            "a huge host delta clamps to the 7-bit magnitude's max"
505        );
506    }
507
508    #[test]
509    fn mouse_speed_cycles_while_strobe_held_high() {
510        let mut mouse = MouseState::default();
511        mouse.latch(true);
512        // Real hardware: every read taken WHILE strobed high cycles the speed setting instead of
513        // shifting real data (ares' `latched==1` branch) — three reads should cycle 0->1->2->0.
514        assert_eq!(mouse.clock(), 0);
515        assert_eq!(mouse.speed, 1);
516        assert_eq!(mouse.clock(), 0);
517        assert_eq!(mouse.speed, 2);
518        assert_eq!(mouse.clock(), 0);
519        assert_eq!(mouse.speed, 0);
520    }
521
522    #[test]
523    fn mouse_free_runs_high_past_32_bits() {
524        let mut mouse = MouseState::default();
525        mouse.set_input(0, 0, false, false);
526        mouse.latch(true);
527        mouse.latch(false);
528        let bits = shift_bits(MouseState::clock, &mut mouse, 40);
529        assert!(
530            bits[32..].iter().all(|&b| b == 1),
531            "past bit 32, reads free-run high"
532        );
533    }
534
535    #[test]
536    fn superscope_trigger_cursor_and_offscreen_bits() {
537        let mut scope = SuperScopeState::default();
538        scope.set_input(100, 100, scope::TRIGGER | scope::CURSOR);
539        scope.latch(true);
540        scope.latch(false);
541        let bits = shift_bits(|s: &mut SuperScopeState| s.clock(224), &mut scope, 8);
542        assert_eq!(
543            bits[0], 1,
544            "trigger fires (aimed on-screen, not turbo-locked yet)"
545        );
546        assert_eq!(bits[1], 1, "cursor is level-sensitive, reads live");
547        assert_eq!(
548            bits[2], 0,
549            "turbo edge hasn't toggled (turbo switch not held)"
550        );
551        assert_eq!(bits[3], 0, "pause hasn't been pressed");
552        assert_eq!(bits[6], 0, "aimed on-screen");
553    }
554
555    #[test]
556    fn superscope_offscreen_suppresses_trigger_and_sets_offscreen_bit() {
557        let mut scope = SuperScopeState::default();
558        scope.set_input(-16, -16, scope::TRIGGER);
559        scope.latch(true);
560        scope.latch(false);
561        let bits = shift_bits(|s: &mut SuperScopeState| s.clock(224), &mut scope, 8);
562        assert_eq!(bits[0], 0, "trigger is suppressed while aimed off-screen");
563        assert_eq!(bits[6], 1, "offscreen bit set");
564        assert!(
565            scope.beam_target(224).is_none(),
566            "no beam target while offscreen"
567        );
568    }
569
570    #[test]
571    fn superscope_beam_target_matches_ares_formula() {
572        let mut scope = SuperScopeState::default();
573        scope.set_input(50, 30, 0);
574        // cx+24 dots on scanline cy (ares' `(cx+24)*4` hcounter target, already dot-space here).
575        assert_eq!(scope.beam_target(224), Some((30, 74)));
576    }
577
578    #[test]
579    fn superscope_turbo_makes_trigger_level_sensitive() {
580        let mut scope = SuperScopeState::default();
581        // Toggle turbo on first, forcing a sample via one throwaway clock.
582        scope.set_input(10, 10, scope::TURBO);
583        scope.latch(true);
584        scope.latch(false);
585        scope.clock(224);
586        assert!(scope.turbo_edge, "turbo toggled on");
587
588        // Now hold trigger across two full latch cycles without releasing it in between — a
589        // plain (non-turbo) trigger would only fire once (edge-sensitive), but with turbo active
590        // it should fire every time (level-sensitive).
591        scope.set_input(10, 10, scope::TURBO | scope::TRIGGER);
592        scope.latch(true);
593        scope.latch(false);
594        let first = scope.clock(224);
595        scope.latch(true);
596        scope.latch(false);
597        let second = scope.clock(224);
598        assert_eq!(first, 1, "trigger fires with turbo active");
599        assert_eq!(
600            second, 1,
601            "trigger keeps firing every latch while held, under turbo"
602        );
603    }
604
605    #[test]
606    fn superscope_free_runs_high_past_8_bits() {
607        let mut scope = SuperScopeState::default();
608        scope.set_input(10, 10, 0);
609        scope.latch(true);
610        scope.latch(false);
611        let bits = shift_bits(|s: &mut SuperScopeState| s.clock(224), &mut scope, 12);
612        assert!(
613            bits[8..].iter().all(|&b| b == 1),
614            "past bit 8, reads free-run high"
615        );
616    }
617
618    #[test]
619    fn multitap_device_detection_signature_while_latched() {
620        let mut tap = MultitapState::default();
621        tap.latch(true);
622        assert_eq!(
623            tap.clock(true),
624            (0, 1),
625            "multitap ID: data1=0, data2=1, while latched"
626        );
627        assert_eq!(
628            tap.clock(false),
629            (0, 1),
630            "signature holds regardless of iobit"
631        );
632    }
633
634    #[test]
635    fn multitap_iobit_selects_the_addressed_pair() {
636        let mut tap = MultitapState::default();
637        tap.set_pad(0, 0x8000); // pad 1: only the MSB (first-shifted-out bit) set.
638        tap.set_pad(1, 0x0000); // pad 2: nothing set.
639        tap.set_pad(2, 0x0000); // pad 3: nothing set.
640        tap.set_pad(3, 0x8000); // pad 4: only the MSB set.
641        tap.latch(false);
642
643        // iobit=true selects pads [0,1]: data1 should read pad1's set MSB, data2 pad2's clear one.
644        assert_eq!(tap.clock(true), (1, 0));
645        // iobit=false selects pads [2,3]: pad 3's untouched MSB is still set (independent
646        // counters — reading the [0,1] pair above must not have advanced pads 2/3 at all).
647        assert_eq!(tap.clock(false), (0, 1));
648    }
649
650    #[test]
651    fn multitap_pad_get_set_round_trips() {
652        let mut tap = MultitapState::default();
653        tap.set_pad(2, 0x1234);
654        assert_eq!(tap.pad(2), 0x1234);
655        assert_eq!(tap.pad(0), 0, "untouched sub-pads stay zero");
656    }
657}