Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_pad/
lib.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Digital / DualShock pad polling via SIO0.
3//!
4//! Talks directly to the SIO0 hardware (no BIOS syscalls) so the
5//! same code works whether we side-load a homebrew (HLE BIOS) or
6//! boot through the real BIOS. The controller protocol is simple
7//! enough that a hand-rolled select + four-byte exchange beats
8//! opening events and waiting on them.
9//!
10//! Typical use from a game loop. The controller port has one owner, the
11//! `ControllerPort` token from `psx_rt::Peripherals::take()`, which every pad
12//! poll and memory-card transfer borrows for the length of its transaction:
13//!
14//! ```text
15//! let mut port = peripherals.controller_port;
16//! let mut reader = psx_pad::PadReader::port1();
17//! let pad = reader.poll_on(&mut port);
18//! if pad.buttons.is_held(psx_pad::button::START) {
19//!     // …
20//! }
21//! ```
22//!
23//! The protocol spec, reproduced from nocash PSX-SPX:
24//!
25//! | `TX` | `RX` | Meaning                            |
26//! |------|------|------------------------------------|
27//! | `01` | `FF` | Address byte / select controller   |
28//! | `42` | `41` | Poll command / digital pad ID low  |
29//! | `00` | `5A` | Fill byte / ID high                |
30//! | `00` | `b0` | Buttons group 1 (active-low)       |
31//! | `00` | `b1` | Buttons group 2 (active-low)       |
32//!
33//! DualShock analog mode uses the same first four bytes but reports
34//! ID low `0x73` and appends four stick bytes:
35//! right X/Y, then left X/Y. Fresh DualShocks boot digital, so games
36//! that require sticks should either call [`enable_analog_on`] or
37//! show an "enable analog mode" prompt when [`PadState::is_analog`]
38//! is false. Programs that require a DualShock call
39//! [`require_analog_on`] at boot: it locks the pad in analog mode, so
40//! the Analog button cannot switch it back, and says whether an
41//! analog-capable pad is there at all (`sdk/docs/PAD-ANALOG.md`).
42//!
43//! Every poll is a complete, ACK-paced packet whose length follows the ID it
44//! reports, so a 0x41 digital frame and a 0x73 analog frame decode the same
45//! buttons. A packet that fails part way reports [`PadMode::Unknown`]; read
46//! through a [`PadReader`] to get the last clean state instead.
47//!
48//! Our [`ButtonState`] stores active-high so `buttons.is_held` feels
49//! natural in game code.
50
51#![no_std]
52#![deny(unsafe_op_in_unsafe_fn)]
53#![warn(missing_docs)]
54
55use psx_hw::sio::sio0;
56use psx_io::controller_port::{ExchangeError, Timing, Transport};
57use psx_io::periph::ControllerPort;
58
59pub use psx_io::controller_port::Port;
60
61pub mod tracker;
62pub use tracker::PadTracker;
63
64// Under test the driver talks to a controller model, not the registers.
65#[cfg(test)]
66mod mock_sio;
67#[cfg(test)]
68mod transport_tests;
69
70/// Named button bitmasks (active-high in this representation).
71/// Hardware's active-low wire format is hidden inside [`poll_port1`].
72pub mod button {
73    /// SELECT.
74    pub const SELECT: u16 = 1 << 0;
75    /// Left stick click (DualShock L3).
76    pub const L3: u16 = 1 << 1;
77    /// Right stick click (DualShock R3).
78    pub const R3: u16 = 1 << 2;
79    /// START.
80    pub const START: u16 = 1 << 3;
81    /// D-pad up.
82    pub const UP: u16 = 1 << 4;
83    /// D-pad right.
84    pub const RIGHT: u16 = 1 << 5;
85    /// D-pad down.
86    pub const DOWN: u16 = 1 << 6;
87    /// D-pad left.
88    pub const LEFT: u16 = 1 << 7;
89    /// L2 shoulder.
90    pub const L2: u16 = 1 << 8;
91    /// R2 shoulder.
92    pub const R2: u16 = 1 << 9;
93    /// L1 shoulder.
94    pub const L1: u16 = 1 << 10;
95    /// R1 shoulder.
96    pub const R1: u16 = 1 << 11;
97    /// Triangle face button.
98    pub const TRIANGLE: u16 = 1 << 12;
99    /// Circle face button.
100    pub const CIRCLE: u16 = 1 << 13;
101    /// Cross (X) face button.
102    pub const CROSS: u16 = 1 << 14;
103    /// Square face button.
104    pub const SQUARE: u16 = 1 << 15;
105}
106
107/// Result of one pad poll. `bits()` gives the raw active-high mask;
108/// [`ButtonState::is_held`] is the ergonomic per-button check.
109#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
110pub struct ButtonState(u16);
111
112impl ButtonState {
113    /// Empty -- nothing held.
114    pub const NONE: Self = Self(0);
115
116    /// Construct from an active-high mask.
117    pub const fn from_bits(bits: u16) -> Self {
118        Self(bits)
119    }
120
121    /// Raw bitmask.
122    #[inline]
123    pub const fn bits(self) -> u16 {
124        self.0
125    }
126
127    /// True when any masked button is down now and none was down previously.
128    /// For multi-button masks this is a group edge, not a per-button edge.
129    #[inline]
130    pub const fn pressed_since(self, previous: Self, mask: u16) -> bool {
131        self.is_held(mask) && !previous.is_held(mask)
132    }
133
134    /// `true` when `mask` (any single-bit [`button`] constant or an
135    /// OR of several) is currently pressed.
136    #[inline]
137    pub const fn is_held(self, mask: u16) -> bool {
138        self.0 & mask != 0
139    }
140}
141
142/// Default analog-stick reading. `0x80` = centred, `0x00` = full
143/// negative, `0xFF` = full positive.
144pub const STICK_CENTER: u8 = 0x80;
145
146/// Controller operating mode inferred from the poll ID byte.
147#[derive(Copy, Clone, Debug, PartialEq, Eq)]
148pub enum PadMode {
149    /// No controller answered the poll.
150    Disconnected,
151    /// SCPH-1080 digital-pad shape: ID `0x41`, buttons only.
152    Digital,
153    /// DualShock analog shape: ID `0x73`, buttons plus stick bytes.
154    Analog,
155    /// DualShock config/escape shape: ID `0xF3`.
156    Config,
157    /// A controller answered with an ID this SDK does not classify yet.
158    Unknown,
159}
160
161impl PadMode {
162    /// `true` when a controller answered the poll.
163    #[inline]
164    pub const fn is_connected(self) -> bool {
165        !matches!(self, Self::Disconnected)
166    }
167
168    /// `true` when the controller is currently reporting stick bytes.
169    #[inline]
170    pub const fn has_sticks(self) -> bool {
171        matches!(self, Self::Analog | Self::Config)
172    }
173}
174
175/// Raw DualShock stick bytes from one poll.
176#[derive(Copy, Clone, Debug, PartialEq, Eq)]
177pub struct AnalogSticks {
178    /// Right stick horizontal axis.
179    pub right_x: u8,
180    /// Right stick vertical axis.
181    pub right_y: u8,
182    /// Left stick horizontal axis.
183    pub left_x: u8,
184    /// Left stick vertical axis.
185    pub left_y: u8,
186}
187
188impl AnalogSticks {
189    /// Centred sticks.
190    pub const CENTERED: Self = Self {
191        right_x: STICK_CENTER,
192        right_y: STICK_CENTER,
193        left_x: STICK_CENTER,
194        left_y: STICK_CENTER,
195    };
196
197    /// Left stick as signed deltas from centre.
198    #[inline]
199    pub const fn left_centered(self) -> (i16, i16) {
200        (
201            self.left_x as i16 - STICK_CENTER as i16,
202            self.left_y as i16 - STICK_CENTER as i16,
203        )
204    }
205
206    /// Right stick as signed deltas from centre.
207    #[inline]
208    pub const fn right_centered(self) -> (i16, i16) {
209        (
210            self.right_x as i16 - STICK_CENTER as i16,
211            self.right_y as i16 - STICK_CENTER as i16,
212        )
213    }
214}
215
216/// A radial dead region around stick centre, with the scaled response the
217/// literature settles on.
218///
219/// Three games on the PSoXide demo disc had grown one of these and they did not
220/// agree. hl-psx tests `x*x + y*y` against a squared radius. VoXide gates each
221/// axis separately, which makes its dead region a square: a stick pushed gently
222/// along a diagonal clears the threshold on one axis and not the other, so the
223/// input snaps to a cardinal instead of going where it was pushed. Sutphin's
224/// write-up calls that the axial dead zone and its snapping is the classic
225/// complaint about it.
226///
227/// A stick's centre drift is radial, so the region that ignores it is a circle,
228/// and [`Deadzone::scaled`] is the scaled-radial form: remapped so the first
229/// real reading starts from zero rather than jumping to the boundary magnitude.
230/// That is the recommended default. [`Deadzone::gate`] is the plain radial one,
231/// kept because the games being replaced were tuned against it.
232///
233/// This is for two-axis input. A single-axis control is a different question
234/// and a scalar threshold is right for it: NitroXide steers on the left stick's
235/// X alone, and folding Y into the test there would let noise on an axis it
236/// does not read enable steering. See [`Deadzone::is_outside_axis`].
237#[derive(Copy, Clone, Debug, PartialEq, Eq)]
238pub struct Deadzone {
239    inner_squared: i32,
240    inner: i16,
241    outer: i16,
242}
243
244/// Full deflection on one axis, from a stick that reads 0..=255 about 128.
245pub const STICK_FULL: i16 = 127;
246
247/// Fine-aim response shared by first-person games.
248///
249/// The centre is deliberately gentle while full deflection remains near the
250/// full input range: 45% linear plus 55% cubic. This is the response used by
251/// `hl-psx`, promoted here so other PSoXide games do not grow subtly different
252/// controller curves.
253#[inline]
254pub const fn aim_curve(value: i16) -> i16 {
255    let value = if value < -128 {
256        -128
257    } else if value > 127 {
258        127
259    } else {
260        value
261    };
262    aim_curve_symmetric(value)
263}
264
265/// Fine-aim curve accepting both -128 and +128, for axes inverted after
266/// centering. Uses the same sequential integer rounding as [`aim_curve`].
267#[inline]
268pub const fn aim_curve_symmetric(value: i16) -> i16 {
269    let value = if value < -128 {
270        -128
271    } else if value > 128 {
272        128
273    } else {
274        value
275    };
276    let sign = if value < 0 { -1 } else { 1 };
277    let magnitude = if value < 0 {
278        -(value as i32)
279    } else {
280        value as i32
281    };
282    let cubic = magnitude * magnitude / 128 * magnitude / 128;
283    (sign * ((magnitude * 45 + cubic * 55) / 100)) as i16
284}
285
286impl Deadzone {
287    /// A dead region of `radius` counts around centre. Typical radii are 12 to
288    /// 30 of the 127 counts a healthy stick reaches. A negative radius is no
289    /// dead region (zero): the squared test would otherwise treat it as its
290    /// absolute value while [`scaled`](Self::scaled) divided by a zero length.
291    pub const fn new(radius: i16) -> Self {
292        let radius = if radius < 0 { 0 } else { radius };
293        Self {
294            inner_squared: (radius as i32) * (radius as i32),
295            inner: radius,
296            outer: STICK_FULL,
297        }
298    }
299
300    /// Treat any deflection at or beyond `outer` as full scale.
301    ///
302    /// A worn stick often cannot reach its corners, so without this the player
303    /// simply never gets full input. Sutphin recommends exposing it as a
304    /// setting for anything where magnitude matters.
305    pub const fn with_outer(mut self, outer: i16) -> Self {
306        self.outer = outer;
307        self
308    }
309
310    /// The inner radius this was built with.
311    pub const fn radius(self) -> i16 {
312        self.inner
313    }
314
315    /// Is this reading real input rather than centre drift?
316    ///
317    /// Strictly outside, matching the boundary behaviour of the games this
318    /// replaces.
319    #[inline]
320    pub const fn is_outside(self, x: i16, y: i16) -> bool {
321        let (x, y) = (x as i32, y as i32);
322        x * x + y * y > self.inner_squared
323    }
324
325    /// The reading unchanged if it is real input, or `None` inside the dead
326    /// region. The plain radial form: direction is preserved, but magnitude
327    /// jumps from zero to the boundary the moment it is crossed.
328    #[inline]
329    pub const fn gate(self, x: i16, y: i16) -> Option<(i16, i16)> {
330        if self.is_outside(x, y) {
331            Some((x, y))
332        } else {
333            None
334        }
335    }
336
337    /// Scaled radial: `None` inside the dead region, otherwise the reading with
338    /// its magnitude remapped from the dead edge onto full scale, keeping
339    /// direction.
340    ///
341    /// The result is clamped to [`STICK_FULL`] per axis. Without that a full
342    /// diagonal, which is 179 counts long rather than 127, remaps to 138 on
343    /// each axis and hands the game 9% more than it believes the stick can
344    /// produce.
345    #[inline]
346    pub fn scaled(self, x: i16, y: i16) -> Option<(i16, i16)> {
347        let (fx, fy) = (x as i32, y as i32);
348        let magnitude = psx_math::int32::isqrt_i32(fx * fx + fy * fy);
349        let inner = self.inner as i32;
350        if magnitude <= inner {
351            return None;
352        }
353        let span = (self.outer as i32 - inner).max(1);
354        let scaled = (((magnitude - inner) * STICK_FULL as i32) / span).min(STICK_FULL as i32);
355        Some((
356            ((fx * scaled) / magnitude) as i16,
357            ((fy * scaled) / magnitude) as i16,
358        ))
359    }
360
361    /// The one-axis form, for a control that reads a single axis.
362    #[inline]
363    pub const fn is_outside_axis(self, v: i16) -> bool {
364        let v = v as i32;
365        v * v > self.inner_squared
366    }
367
368    /// The one-axis form of [`Deadzone::scaled`].
369    #[inline]
370    pub const fn scaled_axis(self, v: i16) -> Option<i16> {
371        if !self.is_outside_axis(v) {
372            return None;
373        }
374        let span = if self.outer > self.inner {
375            (self.outer - self.inner) as i32
376        } else {
377            1
378        };
379        let magnitude = if v < 0 { -(v as i32) } else { v as i32 };
380        let scaled = (magnitude - self.inner as i32) * STICK_FULL as i32 / span;
381        let scaled = if scaled > STICK_FULL as i32 {
382            STICK_FULL as i32
383        } else {
384            scaled
385        };
386        Some(if v < 0 { -scaled as i16 } else { scaled as i16 })
387    }
388}
389
390impl Default for AnalogSticks {
391    fn default() -> Self {
392        Self::CENTERED
393    }
394}
395
396/// Result of one controller poll.
397#[derive(Copy, Clone, Debug, PartialEq, Eq)]
398pub struct PadState {
399    /// Active-high button state.
400    pub buttons: ButtonState,
401    /// Inferred controller mode.
402    pub mode: PadMode,
403    /// Stick bytes. Centred when the controller is not reporting sticks.
404    pub sticks: AnalogSticks,
405    /// Raw low ID byte returned by the controller.
406    pub id_low: u8,
407}
408
409impl PadState {
410    /// No controller connected / no response.
411    pub const NONE: Self = Self {
412        buttons: ButtonState::NONE,
413        mode: PadMode::Disconnected,
414        sticks: AnalogSticks::CENTERED,
415        id_low: 0xFF,
416    };
417
418    /// `true` when a controller answered the poll.
419    #[inline]
420    pub const fn is_connected(self) -> bool {
421        self.mode.is_connected()
422    }
423
424    /// `true` when the controller is in DualShock analog mode.
425    #[inline]
426    pub const fn is_analog(self) -> bool {
427        matches!(self.mode, PadMode::Analog)
428    }
429}
430
431/// One logical game action bound to up to two physical button masks.
432///
433/// A mask may contain more than one button; any held bit activates the
434/// binding. Zero means that binding slot is unused.
435#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
436pub struct ActionBinding {
437    /// Primary physical button mask.
438    pub primary: u16,
439    /// Optional secondary physical button mask.
440    pub secondary: u16,
441}
442
443impl ActionBinding {
444    /// An action with no physical button assigned.
445    pub const UNBOUND: Self = Self::new(0, 0);
446
447    /// Bind an action to primary and secondary button masks.
448    pub const fn new(primary: u16, secondary: u16) -> Self {
449        Self { primary, secondary }
450    }
451
452    #[inline]
453    const fn mask(self) -> u16 {
454        self.primary | self.secondary
455    }
456}
457
458/// Fixed, allocation-free mapping from game-defined action indices to pads.
459#[derive(Copy, Clone, Debug, PartialEq, Eq)]
460pub struct ActionMap<const ACTIONS: usize> {
461    bindings: [ActionBinding; ACTIONS],
462}
463
464impl<const ACTIONS: usize> ActionMap<ACTIONS> {
465    /// Construct a map in the same order as the game's action enum.
466    pub const fn new(bindings: [ActionBinding; ACTIONS]) -> Self {
467        Self { bindings }
468    }
469
470    /// Read the complete binding table.
471    pub const fn bindings(&self) -> &[ActionBinding; ACTIONS] {
472        &self.bindings
473    }
474
475    /// Replace one binding. Out-of-range indices are ignored.
476    pub fn set(&mut self, action: usize, binding: ActionBinding) {
477        if let Some(slot) = self.bindings.get_mut(action) {
478            *slot = binding;
479        }
480    }
481
482    /// Read one binding, returning [`ActionBinding::UNBOUND`] out of range.
483    pub const fn binding(&self, action: usize) -> ActionBinding {
484        if action < ACTIONS {
485            self.bindings[action]
486        } else {
487            ActionBinding::UNBOUND
488        }
489    }
490
491    /// Interpret current and previous pad states through this map.
492    pub const fn input<'a>(
493        &'a self,
494        current: PadState,
495        previous: PadState,
496    ) -> ActionInput<'a, ACTIONS> {
497        ActionInput {
498            map: self,
499            current,
500            previous,
501        }
502    }
503}
504
505/// Per-tick logical action view over a pair of pad samples.
506#[derive(Copy, Clone, Debug)]
507pub struct ActionInput<'a, const ACTIONS: usize> {
508    map: &'a ActionMap<ACTIONS>,
509    current: PadState,
510    previous: PadState,
511}
512
513impl<const ACTIONS: usize> ActionInput<'_, ACTIONS> {
514    /// Whether an action is held now.
515    #[inline]
516    pub fn is_held(self, action: usize) -> bool {
517        let mask = self.map.binding(action).mask();
518        mask != 0 && self.current.buttons.is_held(mask)
519    }
520
521    /// Whether an action transitioned from released to pressed this tick.
522    #[inline]
523    pub fn just_pressed(self, action: usize) -> bool {
524        let mask = self.map.binding(action).mask();
525        mask != 0 && self.current.buttons.is_held(mask) && !self.previous.buttons.is_held(mask)
526    }
527
528    /// Whether an action transitioned from pressed to released this tick.
529    #[inline]
530    pub fn just_released(self, action: usize) -> bool {
531        let mask = self.map.binding(action).mask();
532        mask != 0 && !self.current.buttons.is_held(mask) && self.previous.buttons.is_held(mask)
533    }
534}
535
536/// How a transaction paces its bytes across the serial link.
537#[derive(Copy, Clone, Debug, PartialEq, Eq)]
538pub enum Pacing {
539    /// Wait only for the RX FIFO between bytes; never wait for `/ACK`. This is
540    /// the legacy timing: it works with fast (often third-party) controllers and
541    /// in emulation, but desyncs slower original units (e.g. SCPH-1200) that
542    /// pull `/ACK` low later. Exposed so a diagnostic can reproduce the failure
543    /// side-by-side with [`Pacing::AckWait`].
544    NoAckWait,
545    /// Wait for the device's `/ACK` (DSR) pulse after each non-final byte before
546    /// clocking the next one. Matches the BIOS pacing, but corrupted frames on a
547    /// third-party clone pad, so this IRQ-latched path remains diagnostic.
548    /// Production polling observes live ACK without rewriting CTRL between
549    /// bytes; the analog-enable handshake retains its separate fixed timing.
550    /// Kept so on-console diagnostics can compare the legacy pacings.
551    AckWait,
552}
553
554/// Raw wire result of one poll, before active-low button inversion. Carries the
555/// per-byte `/ACK` observations so a diagnostic can show whether the controller
556/// acknowledges each byte, and which pacing produced a clean handshake.
557#[derive(Copy, Clone, Debug, PartialEq, Eq)]
558pub struct RawPoll {
559    /// ID low byte: 0x41 digital, 0x73 analog, 0xF3 config, 0xFF none.
560    pub id_low: u8,
561    /// ID high byte; 0x5A on a valid controller, anything else is a desync.
562    pub id_high: u8,
563    /// First button byte, raw active-low wire value.
564    pub buttons_low: u8,
565    /// Second button byte, raw active-low wire value.
566    pub buttons_high: u8,
567    /// Raw stick bytes (centre 0x80) when the controller reports them.
568    pub sticks: AnalogSticks,
569    /// Mode inferred from the ID bytes (`Unknown` when the 0x5A magic is wrong).
570    pub mode: PadMode,
571    /// Bit `i` set if exchange `i` observed an `/ACK` pulse (bit 0 = select byte).
572    pub ack_seen: u16,
573    /// Number of exchanges performed (select byte + payload bytes).
574    pub exchanges: u8,
575}
576
577impl RawPoll {
578    /// A poll where nothing answered (id_low `0xFF`). Useful as a const seed.
579    pub const NONE: Self = Self::disconnected(0xFF);
580
581    /// A poll where nothing answered.
582    const fn disconnected(id_low: u8) -> Self {
583        Self {
584            id_low,
585            id_high: 0xFF,
586            buttons_low: 0xFF,
587            buttons_high: 0xFF,
588            sticks: AnalogSticks::CENTERED,
589            mode: PadMode::Disconnected,
590            ack_seen: 0,
591            exchanges: 0,
592        }
593    }
594
595    /// `true` when every payload exchange that should have been acknowledged
596    /// was. The select byte (bit 0) and all bytes up to (but not including) the
597    /// final, never-acknowledged byte must each show an `/ACK`.
598    #[inline]
599    pub fn is_fully_acknowledged(self) -> bool {
600        if self.exchanges < 2 {
601            return false;
602        }
603        // The last exchange is the final packet byte; the device does not ACK
604        // it. Every earlier exchange must have been acknowledged.
605        let acked_needed = self.exchanges - 1;
606        let mask = (1u16 << acked_needed) - 1;
607        self.ack_seen & mask == mask
608    }
609
610    /// Convert to the cleaned [`PadState`] used by game code.
611    pub fn to_state(self) -> PadState {
612        PadState {
613            buttons: decode_buttons(self.buttons_low, self.buttons_high),
614            mode: self.mode,
615            sticks: self.sticks,
616            id_low: self.id_low,
617        }
618    }
619}
620
621// The driver talks to the port through `psx_io::controller_port::Transport`: the
622// production poll and the configuration sequence use its select / exchange /
623// finish steps, the diagnostic pacings below use its register accesses. A
624// transaction needs the port for its whole length, so every entry point takes
625// the `ControllerPort` token by `&mut` (a memory-card transfer borrows it the
626// same way), and `PadReader` keeps no hardware state of its own.
627
628// The controller IRQ stays masked in `I_MASK` (the runtime only unmasks VBlank),
629// but arming it in CTRL is what latches `STAT` bit 9 on the `/ACK` edge for the
630// diagnostic ack-wait pacing.
631const CTRL_ACK: u16 = sio0::ctrl::ACK;
632
633const STAT_TX_READY: u32 = sio0::stat::TX_READY;
634const STAT_RX_NOT_EMPTY: u32 = sio0::stat::RX_NOT_EMPTY;
635const STAT_DSR_LEVEL: u32 = sio0::stat::DSR_LEVEL;
636const STAT_IRQ: u32 = sio0::stat::IRQ;
637
638/// Spin budget waiting for the byte shift itself (TX-ready / RX-not-empty).
639const EXCHANGE_WAIT_SPINS: u32 = Timing::PAD.byte_spins;
640/// Spin budget waiting for the `/ACK` pulse. Comfortably exceeds the kernel's
641/// ~100us DSR timeout on hardware and the emulator's ~1k-cycle ACK deadline,
642/// so a genuinely slow original controller is still given time to answer.
643const ACK_WAIT_SPINS: u32 = Timing::PAD.ack_spins;
644/// Setup delay (bounded STAT reads) after asserting the select line, before the
645/// first clock. The original SCPH-1200 gives NO response without it and a clean
646/// `5A41` digital read with it; fast clones tolerate it either way (silicon,
647/// 2026-06-22). The on-console sweep put the floor between 384 (no response) and
648/// 768 (clean) under a 5-polls-per-frame stress harder than the in-game one poll
649/// per frame, so 1024 is a comfortable margin while keeping the per-poll
650/// busy-wait small. Production retains this measured setup margin and also
651/// waits for each non-final byte's ACK readiness; the BIOS likewise paces bytes.
652pub const DEFAULT_SETUP_SPINS: u32 = Timing::PAD.setup_spins;
653
654/// Poll the controller in `socket` once.
655///
656/// The returned [`PadState`] always contains active-high buttons; in
657/// analog mode it also contains the four DualShock stick bytes. A packet
658/// that cannot be completed after bounded retries reports [`PadMode::Unknown`]
659/// rather than synthetic button bytes. A consistently absent device reports
660/// [`PadMode::Disconnected`] after the bounded acquisition attempts.
661///
662/// `port` is the [`ControllerPort`](psx_io::periph::ControllerPort) token; it
663/// stays borrowed for the length of the poll.
664#[doc(alias = "PadRead")]
665pub fn poll_on<T: Transport>(port: &mut T, socket: Port) -> PadState {
666    poll_state(port, socket)
667}
668
669/// Poll port 1 once.
670#[deprecated(note = "use `poll_on` with the `ControllerPort` token")]
671pub fn poll_port1() -> PadState {
672    poll_on(&mut steal_port(), Port::One)
673}
674
675/// Poll port 2 once.
676#[deprecated(note = "use `poll_on` with the `ControllerPort` token")]
677pub fn poll_port2() -> PadState {
678    poll_on(&mut steal_port(), Port::Two)
679}
680
681/// The token for a deprecated forwarder that never took one.
682fn steal_port() -> ControllerPort {
683    // SAFETY: a token is a logic guard, not a memory-safety one (see
684    // `psx_io::periph`), and the old free functions never took one.
685    unsafe { ControllerPort::steal() }
686}
687
688/// Poll `socket` once and return the raw wire bytes plus `/ACK` observations,
689/// using the requested [`Pacing`]. Intended for diagnostics that want to show
690/// the unfiltered handshake (or reproduce the legacy [`Pacing::NoAckWait`]
691/// failure); normal game code should use [`poll_on`].
692pub fn poll_raw_on<T: Transport>(port: &mut T, socket: Port, pacing: Pacing) -> RawPoll {
693    poll_once_raw(port, socket, pacing)
694}
695
696/// Poll `socket` once with explicit fixed timing, for hardware diagnostics:
697/// `setup_spins` of delay after asserting the select line, plus `interbyte_spins`
698/// of fixed delay after each byte (bounded STAT reads -- no CTRL writes, no
699/// `/ACK` wait, no DSR IRQ). This isolates the two timings a strict original pad
700/// (SCPH-1200) might need -- setup time after `/CS`, and an inter-byte gap --
701/// without the machinery that corrupted the ack-wait path on silicon.
702pub fn poll_diagnostics_on<T: Transport>(
703    port: &mut T,
704    socket: Port,
705    setup_spins: u32,
706    interbyte_spins: u32,
707) -> RawPoll {
708    poll_once_timed(port, socket, setup_spins, interbyte_spins)
709}
710
711/// Poll port 1 once with fixed timing.
712#[deprecated(note = "use `poll_diagnostics_on` with the `ControllerPort` token")]
713pub fn poll_port1_diagnostics(setup_spins: u32, interbyte_spins: u32) -> RawPoll {
714    poll_diagnostics_on(&mut steal_port(), Port::One, setup_spins, interbyte_spins)
715}
716
717/// Ask the controller in `socket` to enter DualShock analog mode. Returns
718/// `true` when a follow-up poll reports analog mode.
719///
720/// Digital-only controllers simply keep reporting digital mode, so
721/// callers should still gate analog-only controls on
722/// [`PadState::is_analog`].
723pub fn enable_analog_on<T: Transport>(port: &mut T, socket: Port) -> bool {
724    enable_analog(port, socket)
725}
726
727/// Ask the port-1 controller to enter analog mode.
728#[deprecated(note = "use `enable_analog_on` with the `ControllerPort` token")]
729pub fn enable_analog_port1() -> bool {
730    enable_analog_on(&mut steal_port(), Port::One)
731}
732
733/// Ask the port-2 controller to enter analog mode.
734#[deprecated(note = "use `enable_analog_on` with the `ControllerPort` token")]
735pub fn enable_analog_port2() -> bool {
736    enable_analog_on(&mut steal_port(), Port::Two)
737}
738
739/// What a port held after [`require_analog_on`] asked it for analog mode.
740#[derive(Copy, Clone, Debug, PartialEq, Eq)]
741pub enum AnalogRequirement {
742    /// A DualShock answered in analog mode, now locked there: the Analog
743    /// button no longer switches it back to digital.
744    Analog,
745    /// A controller answered but did not settle in analog mode: an original
746    /// digital pad, or a DualShock that refused or ignored the request.
747    DigitalOnly,
748    /// No controller gave a clean reply.
749    Absent,
750}
751
752impl AnalogRequirement {
753    /// Classify the mode a pad reported after the analog request.
754    pub const fn from_mode(mode: PadMode) -> Self {
755        match mode {
756            PadMode::Analog => Self::Analog,
757            PadMode::Digital | PadMode::Config => Self::DigitalOnly,
758            PadMode::Disconnected | PadMode::Unknown => Self::Absent,
759        }
760    }
761}
762
763/// Put the controller in `socket` in analog mode, lock it there, and report
764/// what it settled on. For programs that need sticks and must not let the
765/// Analog button flip the pad back to digital mid-session. Call it at boot (a
766/// chain-loaded program starts from whatever the previous one left) and
767/// again while the answer is not [`AnalogRequirement::Analog`], so a pad
768/// plugged in later is picked up.
769pub fn require_analog_on<T: Transport>(port: &mut T, socket: Port) -> AnalogRequirement {
770    AnalogRequirement::from_mode(request_analog(port, socket, REQUIRE_ANALOG_GAP_SPINS).mode)
771}
772
773/// [`require_analog_on`] for port 1.
774#[deprecated(note = "use `require_analog_on` with the `ControllerPort` token")]
775pub fn require_analog_port1() -> AnalogRequirement {
776    require_analog_on(&mut steal_port(), Port::One)
777}
778
779/// A port reader that never hands a garbled packet to the game.
780///
781/// A poll that fails validation reports [`PadMode::Unknown`] with every
782/// button released. Taken at face value, a held button would read as released
783/// for that one frame and then as a fresh press on the next, so a held Select
784/// or Cross fires twice. The reader returns the last clean state instead.
785/// A clean report of an empty port is accepted as it is: unplugging a pad
786/// does release its buttons.
787#[derive(Copy, Clone, Debug)]
788pub struct PadReader {
789    socket: Port,
790    last: PadState,
791}
792
793impl PadReader {
794    /// A reader for port 1 that has seen nothing yet.
795    pub const fn port1() -> Self {
796        Self {
797            socket: Port::One,
798            last: PadState::NONE,
799        }
800    }
801
802    /// A reader for port 2 that has seen nothing yet.
803    pub const fn port2() -> Self {
804        Self {
805            socket: Port::Two,
806            last: PadState::NONE,
807        }
808    }
809
810    /// Poll the port once and return the latest clean state.
811    pub fn poll_on<T: Transport>(&mut self, port: &mut T) -> PadState {
812        let polled = poll_state(port, self.socket);
813        self.accept(polled)
814    }
815
816    /// Poll the port once and return the latest clean state.
817    #[deprecated(note = "use `poll_on` with the `ControllerPort` token")]
818    pub fn poll(&mut self) -> PadState {
819        self.poll_on(&mut steal_port())
820    }
821
822    /// Fold one poll result into the reader and return the latest clean
823    /// state. [`PadReader::poll_on`] calls this; it is public so a caller that
824    /// polls some other way gets the same rule.
825    pub fn accept(&mut self, state: PadState) -> PadState {
826        if state.mode != PadMode::Unknown {
827            self.last = state;
828        }
829        self.last
830    }
831
832    /// The latest clean state without polling.
833    pub const fn last(&self) -> PadState {
834        self.last
835    }
836}
837
838/// Poll a port, retrying a garbled response.
839///
840/// On real hardware a poll occasionally desyncs -- a stale byte lingering in
841/// the RX FIFO from the previous transaction, or a slipped /ACK between bytes --
842/// and the ID handshake comes back wrong. A single such frame reads as "every
843/// button released", which makes a *held* button (jump, Start) look like a fresh
844/// press the next frame. So: when a controller answered but the DualShock ID
845/// handshake didn't validate, retry a few times and take the first clean read.
846/// Transport failures are retried as whole transactions. Four address-byte
847/// replies of FF without ACK report Disconnected; a failure at any other
848/// stage reports Unknown instead of accepting partial button bytes.
849fn poll_state<T: Transport>(bus: &mut T, socket: Port) -> PadState {
850    let mut last = PadState::NONE;
851    let mut all_absent = true;
852    let mut tries = 0;
853    while tries < 4 {
854        let s = poll_once(bus, socket).to_state();
855        if matches!(s.mode, PadMode::Digital | PadMode::Analog | PadMode::Config) {
856            return s;
857        }
858        all_absent &= s.mode == PadMode::Disconnected;
859        last = s;
860        tries += 1;
861    }
862    // Only four complete address-byte replies of FF without ACK establish
863    // an absent device. Any other failed stage means an invalid transaction.
864    if !all_absent {
865        last.mode = PadMode::Unknown;
866    }
867    last
868}
869
870fn poll_once_raw<T: Transport>(bus: &mut T, socket: Port, pacing: Pacing) -> RawPoll {
871    // Raise JOYN so the device's state machine starts from idle, then drain
872    // any stale RX byte. Only the ack-wait path arms the DSR IRQ.
873    bus.select_with(socket, matches!(pacing, Pacing::AckWait));
874    bus.drain_receive();
875
876    let mut ack = 0u16;
877    let mut i = 0u8;
878
879    // The select byte and the poll command are never the final byte of any
880    // packet, so they are always `/ACK`-paced under `AckWait`.
881    let _select = ex(bus, socket, pacing, 0x01, false, &mut ack, &mut i);
882    let id_low = ex(bus, socket, pacing, 0x42, false, &mut ack, &mut i);
883    let mut mode = mode_from_id_low(id_low);
884    if !mode.is_connected() {
885        bus.deselect();
886        return RawPoll {
887            exchanges: i,
888            ack_seen: ack,
889            ..RawPoll::disconnected(id_low)
890        };
891    }
892
893    let analog = mode.has_sticks();
894    let id_high = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
895    if id_high != 0x5A {
896        // Garbled handshake -- treat as Unknown and stop after the two
897        // button bytes (we can no longer trust the reported length).
898        mode = PadMode::Unknown;
899    }
900
901    // For a digital pad the second button byte is the final byte (no `/ACK`
902    // follows). For an analog pad the four stick bytes come after it.
903    let read_sticks = analog && mode != PadMode::Unknown;
904    let b0 = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
905    let b1 = ex(bus, socket, pacing, 0x00, !read_sticks, &mut ack, &mut i);
906
907    let sticks = if read_sticks {
908        let right_x = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
909        let right_y = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
910        let left_x = ex(bus, socket, pacing, 0x00, false, &mut ack, &mut i);
911        let left_y = ex(bus, socket, pacing, 0x00, true, &mut ack, &mut i);
912        AnalogSticks {
913            right_x,
914            right_y,
915            left_x,
916            left_y,
917        }
918    } else {
919        AnalogSticks::CENTERED
920    };
921
922    bus.deselect();
923
924    RawPoll {
925        id_low,
926        id_high,
927        buttons_low: b0,
928        buttons_high: b1,
929        sticks,
930        mode,
931        ack_seen: ack,
932        exchanges: i,
933    }
934}
935
936/// Clock one byte of a transaction under the given pacing, recording whether the
937/// device acknowledged it. `is_last` marks the final byte of the packet, which
938/// the device never acknowledges, so it is always sent without an `/ACK` wait.
939#[inline]
940fn ex<T: Transport>(
941    bus: &mut T,
942    socket: Port,
943    pacing: Pacing,
944    tx: u8,
945    is_last: bool,
946    ack_seen: &mut u16,
947    idx: &mut u8,
948) -> u8 {
949    let i = *idx;
950    *idx = idx.wrapping_add(1);
951    match pacing {
952        Pacing::NoAckWait => exchange_nowait(bus, tx),
953        Pacing::AckWait if is_last => exchange_nowait(bus, tx),
954        Pacing::AckWait => {
955            let (byte, acked) = exchange_ack(bus, socket, tx);
956            if acked && i < 16 {
957                *ack_seen |= 1u16 << i;
958            }
959            byte
960        }
961    }
962}
963
964/// The production poll: retain the measured post-select setup delay, then
965/// wait for each non-final byte's live ACK assertion and release. RX-ready
966/// only establishes that the current byte arrived, not that the controller
967/// is ready for the next one. No IRQ enable or CTRL rewrite is needed.
968fn poll_once<T: Transport>(bus: &mut T, socket: Port) -> RawPoll {
969    // A previous aborted transaction may have left ACK asserted: `begin` waits
970    // for it to release, so an old pulse never counts as the new address
971    // byte's ACK.
972    let result = if bus.begin(socket, Timing::PAD) {
973        poll_selected(bus)
974    } else {
975        None
976    };
977    // End the peripheral transaction on every path. On a failed byte, also
978    // reset the deselected UART: deselect alone need not cancel a late RX byte
979    // still in its shifter/FIFO. The next select restores MODE and BAUD before
980    // asserting the port again.
981    bus.finish(result.is_some());
982    result.unwrap_or(RawPoll {
983        mode: PadMode::Unknown,
984        ..RawPoll::NONE
985    })
986}
987
988/// A complete selected-port poll, or no usable packet. The current ID
989/// determines the length; a mode toggle never reuses an earlier length.
990fn poll_selected<T: Transport>(bus: &mut T) -> Option<RawPoll> {
991    match bus.exchange(0x01, false, Timing::PAD) {
992        Ok(_) => {}
993        Err(ExchangeError::AckTimeout { reply: 0xFF }) => return Some(RawPoll::NONE),
994        Err(_) => return None,
995    }
996    let id_low = bus.exchange(0x42, false, Timing::PAD).ok()?;
997    let mode = mode_from_id_low(id_low);
998    if matches!(mode, PadMode::Unknown | PadMode::Disconnected) {
999        return None;
1000    }
1001    let id_high = bus.exchange(0x00, false, Timing::PAD).ok()?;
1002    if id_high != 0x5A {
1003        return None;
1004    }
1005    let analog = mode.has_sticks();
1006    let buttons_low = bus.exchange(0x00, false, Timing::PAD).ok()?;
1007    let buttons_high = bus.exchange(0x00, !analog, Timing::PAD).ok()?;
1008    let sticks = if analog {
1009        AnalogSticks {
1010            right_x: bus.exchange(0x00, false, Timing::PAD).ok()?,
1011            right_y: bus.exchange(0x00, false, Timing::PAD).ok()?,
1012            left_x: bus.exchange(0x00, false, Timing::PAD).ok()?,
1013            left_y: bus.exchange(0x00, true, Timing::PAD).ok()?,
1014        }
1015    } else {
1016        AnalogSticks::CENTERED
1017    };
1018    Some(RawPoll {
1019        id_low,
1020        id_high,
1021        buttons_low,
1022        buttons_high,
1023        sticks,
1024        mode,
1025        ack_seen: if analog { 0xff } else { 0x0f },
1026        exchanges: if analog { 9 } else { 5 },
1027    })
1028}
1029
1030/// Poll with fixed setup and inter-byte delays (no `/ACK` wait). Mirrors
1031/// [`poll_once_raw`]'s byte sequence but paces purely with time. Always inlined
1032/// so each caller above is specialised on its own timing.
1033#[inline(always)]
1034fn poll_once_timed<T: Transport>(
1035    bus: &mut T,
1036    socket: Port,
1037    setup_spins: u32,
1038    interbyte_spins: u32,
1039) -> RawPoll {
1040    bus.select(socket);
1041    // Setup time after asserting /CS, before the first clock -- the strict
1042    // original pad may need this where a fast clone does not.
1043    bus.delay(setup_spins);
1044    bus.drain_receive();
1045
1046    let mut i = 0u8;
1047    let _select = exchange_delayed(bus, 0x01, interbyte_spins);
1048    i += 1;
1049    let id_low = exchange_delayed(bus, 0x42, interbyte_spins);
1050    i += 1;
1051    let mut mode = mode_from_id_low(id_low);
1052    if !mode.is_connected() {
1053        bus.deselect();
1054        return RawPoll {
1055            exchanges: i,
1056            ..RawPoll::disconnected(id_low)
1057        };
1058    }
1059
1060    let analog = mode.has_sticks();
1061    let id_high = exchange_delayed(bus, 0x00, interbyte_spins);
1062    i += 1;
1063    if id_high != 0x5A {
1064        mode = PadMode::Unknown;
1065    }
1066    let read_sticks = analog && mode != PadMode::Unknown;
1067    let b0 = exchange_delayed(bus, 0x00, interbyte_spins);
1068    i += 1;
1069    let b1 = exchange_delayed(bus, 0x00, interbyte_spins);
1070    i += 1;
1071
1072    let sticks = if read_sticks {
1073        let right_x = exchange_delayed(bus, 0x00, interbyte_spins);
1074        let right_y = exchange_delayed(bus, 0x00, interbyte_spins);
1075        let left_x = exchange_delayed(bus, 0x00, interbyte_spins);
1076        let left_y = exchange_delayed(bus, 0x00, interbyte_spins);
1077        i += 4;
1078        AnalogSticks {
1079            right_x,
1080            right_y,
1081            left_x,
1082            left_y,
1083        }
1084    } else {
1085        AnalogSticks::CENTERED
1086    };
1087
1088    bus.deselect();
1089
1090    RawPoll {
1091        id_low,
1092        id_high,
1093        buttons_low: b0,
1094        buttons_high: b1,
1095        sticks,
1096        mode,
1097        ack_seen: 0,
1098        exchanges: i,
1099    }
1100}
1101
1102/// Spins between the configuration commands of [`enable_analog`], a few
1103/// milliseconds each. Sony's libpad spaces configuration commands one per
1104/// video frame; issuing the three back to back left an SCPH-110 (PS one
1105/// DualShock) parked in configuration mode on a console, answering ID 0xF3
1106/// with buttons and sticks intact but never analog (controller-test capture,
1107/// 2026-08-16), while an SCPH-1200 took the same burst fine. Not yet
1108/// re-measured on silicon.
1109const CONFIG_COMMAND_GAP_SPINS: u32 = 8 * DEFAULT_SETUP_SPINS;
1110
1111/// Spins between the configuration commands of [`require_analog_on`]:
1112/// about one video frame, the spacing Sony's libpad uses, so the request
1113/// that a program must win (the SCPH-110 is the pad that needed spacing at
1114/// all) uses the reference pacing rather than the shorter, unmeasured one
1115/// above. A spin costs about 6.7 CPU cycles on silicon (the 2026-07-26
1116/// setup-delay sweep: 192 spins polled in 8587 cycles, 1536 in 17615), so
1117/// this is about 660k cycles, a little over one 60 Hz frame. Only boot-time
1118/// and redetect callers pay it; [`enable_analog_on`] keeps its short gap
1119/// because games call it from their frame loops.
1120const REQUIRE_ANALOG_GAP_SPINS: u32 = 96 * DEFAULT_SETUP_SPINS;
1121
1122/// Exit-config retries when a pad is still answering ID 0xF3 after the
1123/// analog request. Each costs one gap; a pad left in configuration mode
1124/// still reports buttons, but never analog, and ignores the Analog lock.
1125const CONFIG_EXIT_RETRIES: u32 = 3;
1126
1127fn enable_analog<T: Transport>(bus: &mut T, socket: Port) -> bool {
1128    request_analog(bus, socket, CONFIG_COMMAND_GAP_SPINS).is_analog()
1129}
1130
1131/// Send the analog-and-lock request, spacing the commands by `gap` spins,
1132/// and return the state the pad settled on. A pad still parked in
1133/// configuration mode (ID 0xF3, the SCPH-110 failure) is sent the exit
1134/// command again, a bounded number of times, before its state is reported.
1135fn request_analog<T: Transport>(bus: &mut T, socket: Port, gap: u32) -> PadState {
1136    // Enter config mode.
1137    transaction(
1138        bus,
1139        socket,
1140        [0x43, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00],
1141    );
1142    bus.delay(gap);
1143    // Request analog mode and lock it so the pad cannot toggle
1144    // back underneath analog-only game controls.
1145    transaction(
1146        bus,
1147        socket,
1148        [0x44, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00],
1149    );
1150    bus.delay(gap);
1151    // Exit config mode, restoring the requested analog mode.
1152    transaction(
1153        bus,
1154        socket,
1155        [0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1156    );
1157    bus.delay(gap);
1158    let mut state = poll_state(bus, socket);
1159    let mut retries = 0;
1160    while state.mode == PadMode::Config && retries < CONFIG_EXIT_RETRIES {
1161        // The exit did not take: leave the pad in a playable mode rather
1162        // than parked in configuration, then re-read what it settled on.
1163        transaction(
1164            bus,
1165            socket,
1166            [0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1167        );
1168        bus.delay(gap);
1169        state = poll_state(bus, socket);
1170        retries += 1;
1171    }
1172    state
1173}
1174
1175#[inline]
1176fn mode_from_id_low(id_low: u8) -> PadMode {
1177    match id_low {
1178        0x41 => PadMode::Digital,
1179        0x73 => PadMode::Analog,
1180        0xF3 => PadMode::Config,
1181        0xFF => PadMode::Disconnected,
1182        _ => PadMode::Unknown,
1183    }
1184}
1185
1186#[inline]
1187fn decode_buttons(b0: u8, b1: u8) -> ButtonState {
1188    // Wire bytes are active-low; invert to match our active-high
1189    // ButtonState convention.
1190    ButtonState::from_bits(!((b0 as u16) | ((b1 as u16) << 8)))
1191}
1192
1193/// CTRL value held while a port is selected: JOYN asserted, TX enabled, and
1194/// normal receive supplied implicitly by the active `/CS`,
1195/// (only when `ack_irq`) the DSR (`/ACK`) interrupt armed so STAT bit 9 latches
1196/// each pulse. The default no-wait poll leaves the DSR IRQ off -- enabling it
1197/// turned out to disturb real-hardware transfers (the official SCPH-1200 stopped
1198/// answering, the clone's bytes corrupted), so it is reserved for the opt-in
1199/// ack-wait diagnostic only.
1200#[inline]
1201const fn active_ctrl(socket: Port, ack_irq: bool) -> u16 {
1202    sio0::selected_ctrl(socket.is_two(), ack_irq)
1203}
1204
1205/// Run a fixed eight-byte DualShock command after the port-level controller
1206/// select byte, using the default no-wait timing (matching the reverted poll
1207/// path).
1208#[inline]
1209fn transaction<T: Transport>(bus: &mut T, socket: Port, bytes: [u8; 8]) -> [u8; 8] {
1210    bus.select(socket);
1211    bus.delay(DEFAULT_SETUP_SPINS);
1212    let mut ack = 0u16;
1213    let mut idx = 0u8;
1214    let _select = ex(
1215        bus,
1216        socket,
1217        Pacing::NoAckWait,
1218        0x01,
1219        false,
1220        &mut ack,
1221        &mut idx,
1222    );
1223    let mut out = [0u8; 8];
1224    let mut i = 0;
1225    while i < bytes.len() {
1226        let is_last = i == bytes.len() - 1;
1227        out[i] = ex(
1228            bus,
1229            socket,
1230            Pacing::NoAckWait,
1231            bytes[i],
1232            is_last,
1233            &mut ack,
1234            &mut idx,
1235        );
1236        i += 1;
1237    }
1238    bus.deselect();
1239    out
1240}
1241
1242/// Clock one byte across the serial link without waiting for `/ACK`: wait for
1243/// TX-ready, write the byte, wait for RX to fill, read it. This is the legacy
1244/// timing -- correct for the final byte of a packet (which is never
1245/// acknowledged) and for the [`Pacing::NoAckWait`] diagnostic path.
1246///
1247/// The STAT register layout (from PSX-SPX):
1248/// - bit 0: TX-ready-1 (FIFO can accept a byte)
1249/// - bit 1: RX FIFO not empty
1250/// - bit 7: `/ACK` (DSR) live input level
1251/// - bit 9: latched DSR/ACK interrupt
1252#[inline]
1253fn exchange_nowait<T: Transport>(bus: &mut T, tx: u8) -> u8 {
1254    if !bus.wait_status_set(STAT_TX_READY, EXCHANGE_WAIT_SPINS) {
1255        return 0xFF;
1256    }
1257    bus.transmit(tx);
1258    if !bus.wait_status_set(STAT_RX_NOT_EMPTY, EXCHANGE_WAIT_SPINS) {
1259        return 0xFF;
1260    }
1261    bus.receive()
1262}
1263
1264/// One no-wait byte exchange followed by a fixed inter-byte delay, giving a
1265/// strict pad time to be ready for the next byte without any `/ACK`/CTRL games.
1266#[inline]
1267fn exchange_delayed<T: Transport>(bus: &mut T, tx: u8, interbyte_spins: u32) -> u8 {
1268    let rx = exchange_nowait(bus, tx);
1269    bus.delay(interbyte_spins);
1270    rx
1271}
1272
1273/// Clock one byte, then wait for the device's `/ACK` (DSR) pulse before
1274/// returning so the caller does not clock the next byte until the controller is
1275/// ready. Returns `(rx_byte, ack_observed)`.
1276///
1277/// The wait is non-fatal: on timeout we still return the received byte (the
1278/// byte shift itself already completed), so a device that never `/ACK`s degrades
1279/// to legacy timing rather than dropping the poll entirely.
1280#[inline]
1281fn exchange_ack<T: Transport>(bus: &mut T, socket: Port, tx: u8) -> (u8, bool) {
1282    if !bus.wait_status_set(STAT_TX_READY, EXCHANGE_WAIT_SPINS) {
1283        return (0xFF, false);
1284    }
1285    bus.transmit(tx);
1286    if !bus.wait_status_set(STAT_RX_NOT_EMPTY, EXCHANGE_WAIT_SPINS) {
1287        return (0xFF, false);
1288    }
1289    let rx = bus.receive();
1290    // Wait for the latched `/ACK` interrupt (STAT bit 9). The latch cannot be
1291    // missed, unlike the brief live level; the controller IRQ is masked in
1292    // `I_MASK`, so this never reaches the CPU.
1293    let acked = bus.wait_status_set(STAT_IRQ, ACK_WAIT_SPINS);
1294    if acked {
1295        // SIO0 STAT.9 is not edge-triggered: it can only be cleared once the
1296        // live `/ACK` line has released (STAT.7 low). Wait for that, then
1297        // pulse CTRL.ACK while keeping JOYN asserted so the device stays
1298        // selected for the next byte.
1299        let _ = bus.wait_status_clear(STAT_DSR_LEVEL, ACK_WAIT_SPINS);
1300        bus.set_control(active_ctrl(socket, true) | CTRL_ACK);
1301    }
1302    (rx, acked)
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307
1308    #[test]
1309    fn symmetric_aim_and_group_edges_preserve_axis_policy() {
1310        for v in -128..=128 {
1311            assert_eq!(aim_curve_symmetric(v), -aim_curve_symmetric(-v));
1312        }
1313        assert_eq!(aim_curve_symmetric(128), 128);
1314        assert_eq!(aim_curve(128), 125);
1315        let a = ButtonState::from_bits(1);
1316        let b = ButtonState::from_bits(2);
1317        assert!(a.pressed_since(ButtonState::NONE, 3));
1318        assert!(!a.pressed_since(b, 3));
1319        assert!(a.pressed_since(b, 1));
1320    }
1321
1322    #[test]
1323    fn a_radial_deadzone_is_a_circle_not_a_square() {
1324        // The whole reason this exists. A per-axis threshold of 24, which is
1325        // what NitroXide used, passes (20, 20) because neither axis clears it
1326        // on its own -- so a gentle diagonal push reads as nothing, and the
1327        // moment one axis does clear it the input snaps to a cardinal. Radially
1328        // (20, 20) is 28.3 counts out and is real input.
1329        let dz = Deadzone::new(24);
1330        assert!(dz.is_outside(20, 20), "diagonal push must count as input");
1331        assert!(
1332            !dz.is_outside(20, 0),
1333            "same magnitude per axis is still drift"
1334        );
1335        assert!(!dz.is_outside(0, 0));
1336        assert!(dz.is_outside(25, 0));
1337    }
1338
1339    #[test]
1340    fn a_negative_deadzone_radius_is_no_deadzone_not_a_division_by_zero() {
1341        // `scaled(0, 0)` passed the `magnitude <= inner` test with inner = -1
1342        // and divided by a zero magnitude.
1343        for radius in [-1, -24, i16::MIN] {
1344            let dz = Deadzone::new(radius);
1345            assert_eq!(dz.radius(), 0);
1346            assert_eq!(dz, Deadzone::new(0));
1347            assert_eq!(dz.scaled(0, 0), None);
1348            assert_eq!(dz.scaled_axis(0), None);
1349            assert!(!dz.is_outside(0, 0));
1350            // A real deflection behaves as it does with a radius of zero.
1351            assert_eq!(dz.scaled(60, 0), Deadzone::new(0).scaled(60, 0));
1352            assert!(dz.is_outside(1, 0));
1353        }
1354    }
1355
1356    #[test]
1357    fn the_one_axis_form_ignores_the_other_axis() {
1358        // NitroXide's steering: Y is not read, so it must not gate X.
1359        let dz = Deadzone::new(24);
1360        assert!(dz.is_outside_axis(30));
1361        assert!(!dz.is_outside_axis(20));
1362        // Rescaled onto full span like the two-axis form, not merely offset:
1363        // (30 - 24) of the 103 counts left maps to 7 of 127.
1364        assert_eq!(dz.scaled_axis(30), Some(7), "starts from zero, not 30");
1365        assert_eq!(dz.scaled_axis(-30), Some(-7), "sign survives");
1366        assert_eq!(dz.scaled_axis(127), Some(STICK_FULL), "full stays full");
1367        assert_eq!(dz.scaled_axis(10), None);
1368    }
1369
1370    #[test]
1371    fn the_gate_hands_back_the_reading_unchanged() {
1372        let dz = Deadzone::new(16);
1373        assert_eq!(dz.gate(4, 4), None);
1374        assert_eq!(dz.gate(100, -40), Some((100, -40)));
1375    }
1376
1377    #[test]
1378    fn scaling_starts_real_input_from_zero_instead_of_the_boundary() {
1379        // Ungated, a reading one count outside a radius of 30 arrives at
1380        // magnitude 31: a lurch. Scaled, it starts near zero and keeps its
1381        // direction.
1382        let dz = Deadzone::new(30);
1383        let (x, y) = dz.scaled(31, 0).expect("just outside is real input");
1384        assert!(x < 5, "expected a small first step, got {x}");
1385        assert_eq!(dz.scaled(127, 0), Some((STICK_FULL, 0)), "full stays full");
1386        assert_eq!(y, 0, "direction must survive the rescale");
1387        assert_eq!(dz.scaled(10, 10), None);
1388    }
1389
1390    #[test]
1391    fn a_full_diagonal_does_not_exceed_full_scale() {
1392        // A diagonal is 179 counts long, not 127. Remapping that onto full
1393        // scale without a clamp hands the game 138 on each axis and it has no
1394        // reason to expect anything over 127.
1395        let dz = Deadzone::new(30);
1396        let (x, y) = dz.scaled(127, 127).expect("full deflection is input");
1397        assert!(x <= STICK_FULL && y <= STICK_FULL, "got ({x}, {y})");
1398        assert_eq!(x, y, "a diagonal must stay a diagonal");
1399    }
1400
1401    #[test]
1402    fn an_outer_zone_lets_a_worn_stick_reach_full_scale() {
1403        // A stick that only manages 100 counts never gives full input without
1404        // this, which is the tip Sutphin gives for anything where magnitude
1405        // matters.
1406        let worn = Deadzone::new(20).with_outer(100);
1407        assert_eq!(worn.scaled(100, 0), Some((STICK_FULL, 0)));
1408        // And past the outer edge it saturates rather than overshooting.
1409        assert_eq!(worn.scaled(120, 0), Some((STICK_FULL, 0)));
1410    }
1411
1412    #[test]
1413    fn aim_curve_is_gentle_at_center_and_near_full_at_the_edge() {
1414        assert_eq!(aim_curve(0), 0);
1415        assert_eq!(aim_curve(16), 7);
1416        assert_eq!(aim_curve(-16), -7);
1417        assert_eq!(aim_curve(127), 125);
1418        assert_eq!(aim_curve(-128), -128);
1419        assert!(aim_curve(64) < 64);
1420        assert_eq!(aim_curve(-64), -aim_curve(64));
1421    }
1422
1423    use super::*;
1424
1425    #[test]
1426    fn mode_ids_match_dualshock_wire_values() {
1427        assert_eq!(mode_from_id_low(0x41), PadMode::Digital);
1428        assert_eq!(mode_from_id_low(0x73), PadMode::Analog);
1429        assert_eq!(mode_from_id_low(0xF3), PadMode::Config);
1430        assert_eq!(mode_from_id_low(0xFF), PadMode::Disconnected);
1431        assert_eq!(mode_from_id_low(0x12), PadMode::Unknown);
1432    }
1433
1434    #[test]
1435    fn decode_buttons_converts_active_low_to_active_high() {
1436        let state = decode_buttons(0xEF, 0xBF);
1437        assert!(state.is_held(button::UP));
1438        assert!(state.is_held(button::CROSS));
1439        assert!(!state.is_held(button::DOWN));
1440    }
1441
1442    #[test]
1443    fn centred_stick_helpers_return_zero() {
1444        assert_eq!(AnalogSticks::CENTERED.left_centered(), (0, 0));
1445        assert_eq!(AnalogSticks::CENTERED.right_centered(), (0, 0));
1446    }
1447
1448    #[test]
1449    fn raw_poll_to_state_inverts_buttons() {
1450        let raw = RawPoll {
1451            id_low: 0x41,
1452            id_high: 0x5A,
1453            buttons_low: 0xEF,  // UP pressed (active-low)
1454            buttons_high: 0xBF, // CROSS pressed (active-low)
1455            sticks: AnalogSticks::CENTERED,
1456            mode: PadMode::Digital,
1457            ack_seen: 0,
1458            exchanges: 5,
1459        };
1460        let state = raw.to_state();
1461        assert!(state.buttons.is_held(button::UP));
1462        assert!(state.buttons.is_held(button::CROSS));
1463        assert_eq!(state.mode, PadMode::Digital);
1464        assert_eq!(state.id_low, 0x41);
1465    }
1466
1467    #[test]
1468    fn ack_complete_requires_every_non_final_byte_acked() {
1469        // Digital poll: 5 exchanges (select, cmd, id_hi, b0, b1). The first four
1470        // must ACK; the final b1 does not.
1471        let mut raw = RawPoll {
1472            id_low: 0x41,
1473            id_high: 0x5A,
1474            buttons_low: 0xFF,
1475            buttons_high: 0xFF,
1476            sticks: AnalogSticks::CENTERED,
1477            mode: PadMode::Digital,
1478            ack_seen: 0b0_1111, // exchanges 0..=3 acked
1479            exchanges: 5,
1480        };
1481        assert!(raw.is_fully_acknowledged());
1482
1483        // Drop one ACK in the middle -> incomplete handshake.
1484        raw.ack_seen = 0b0_1011;
1485        assert!(!raw.is_fully_acknowledged());
1486
1487        // A no-ack poll (legacy / slow original under NoAckWait) is incomplete.
1488        raw.ack_seen = 0;
1489        assert!(!raw.is_fully_acknowledged());
1490    }
1491
1492    #[test]
1493    fn pacing_round_trips() {
1494        assert_ne!(Pacing::NoAckWait, Pacing::AckWait);
1495    }
1496
1497    #[test]
1498    fn action_map_unifies_primary_secondary_and_edges() {
1499        let map = ActionMap::new([
1500            ActionBinding::new(button::CROSS, button::CIRCLE),
1501            ActionBinding::new(button::START, 0),
1502        ]);
1503        let previous = PadState {
1504            buttons: ButtonState::from_bits(button::CROSS),
1505            mode: PadMode::Digital,
1506            sticks: AnalogSticks::CENTERED,
1507            id_low: 0x41,
1508        };
1509        let current = PadState {
1510            buttons: ButtonState::from_bits(button::CIRCLE | button::START),
1511            ..previous
1512        };
1513        let input = map.input(current, previous);
1514        assert!(input.is_held(0));
1515        assert!(!input.just_pressed(0), "the logical action stayed held");
1516        assert!(input.just_pressed(1));
1517        assert!(!input.just_released(0));
1518        assert!(!input.is_held(99));
1519    }
1520
1521    fn state(mode: PadMode, buttons: u16) -> PadState {
1522        PadState {
1523            buttons: ButtonState::from_bits(buttons),
1524            mode,
1525            sticks: AnalogSticks::CENTERED,
1526            id_low: match mode {
1527                PadMode::Digital => 0x41,
1528                PadMode::Analog => 0x73,
1529                PadMode::Config => 0xF3,
1530                _ => 0xFF,
1531            },
1532        }
1533    }
1534
1535    #[test]
1536    fn analog_requirement_classifies_every_mode() {
1537        use AnalogRequirement as R;
1538        assert_eq!(R::from_mode(PadMode::Analog), R::Analog);
1539        assert_eq!(R::from_mode(PadMode::Digital), R::DigitalOnly);
1540        assert_eq!(R::from_mode(PadMode::Config), R::DigitalOnly);
1541        assert_eq!(R::from_mode(PadMode::Disconnected), R::Absent);
1542        assert_eq!(R::from_mode(PadMode::Unknown), R::Absent);
1543    }
1544
1545    #[test]
1546    fn analog_and_digital_frames_decode_the_same_buttons() {
1547        let digital = RawPoll {
1548            id_low: 0x41,
1549            id_high: 0x5A,
1550            buttons_low: 0xFE,  // SELECT (active-low)
1551            buttons_high: 0xBF, // CROSS
1552            sticks: AnalogSticks::CENTERED,
1553            mode: PadMode::Digital,
1554            ack_seen: 0x0f,
1555            exchanges: 5,
1556        };
1557        let sticks = AnalogSticks {
1558            right_x: 0x80,
1559            right_y: 0x80,
1560            left_x: 0x00,
1561            left_y: 0xFF,
1562        };
1563        let analog = RawPoll {
1564            id_low: 0x73,
1565            sticks,
1566            mode: PadMode::Analog,
1567            ack_seen: 0xff,
1568            exchanges: 9,
1569            ..digital
1570        };
1571        let (d, a) = (digital.to_state(), analog.to_state());
1572        assert_eq!(d.buttons, a.buttons);
1573        assert_eq!(a.buttons.bits(), button::SELECT | button::CROSS);
1574        assert!(a.is_analog() && !d.is_analog());
1575        assert_eq!(a.sticks, sticks);
1576        assert_eq!(d.sticks, AnalogSticks::CENTERED);
1577    }
1578
1579    #[test]
1580    fn a_packet_slipped_by_one_byte_reads_as_select_and_both_directions() {
1581        // Why every packet must be complete and paced: an analog packet that
1582        // slips one byte delivers the ID's 0x5A as the first button byte.
1583        // That decodes as Select, R3, Left and Right held together, the
1584        // phantom-Select pattern seen on a console in analog mode.
1585        let slipped = decode_buttons(0x5A, 0xFF);
1586        assert_eq!(
1587            slipped.bits(),
1588            button::SELECT | button::R3 | button::LEFT | button::RIGHT
1589        );
1590    }
1591
1592    #[test]
1593    fn reader_keeps_the_last_clean_state_across_a_garbled_poll() {
1594        let mut reader = PadReader::port1();
1595        let mut tracker = PadTracker::new();
1596        let held = state(PadMode::Analog, button::SELECT);
1597        tracker.update(reader.accept(held).buttons.bits());
1598        assert!(tracker.just_pressed(button::SELECT));
1599        // The failed packet must neither release nor re-press Select.
1600        let garbled = PadState {
1601            mode: PadMode::Unknown,
1602            ..PadState::NONE
1603        };
1604        let seen = reader.accept(garbled);
1605        assert_eq!(seen, held);
1606        tracker.update(seen.buttons.bits());
1607        assert!(!tracker.just_released(button::SELECT));
1608        tracker.update(reader.accept(held).buttons.bits());
1609        assert!(!tracker.just_pressed(button::SELECT), "no second press");
1610    }
1611
1612    #[test]
1613    fn reader_accepts_every_clean_mode_including_an_empty_port() {
1614        let mut reader = PadReader::port1();
1615        assert_eq!(reader.last(), PadState::NONE);
1616        for clean in [
1617            state(PadMode::Digital, button::CROSS),
1618            state(PadMode::Analog, button::START),
1619            state(PadMode::Config, 0),
1620            PadState::NONE,
1621        ] {
1622            assert_eq!(reader.accept(clean), clean);
1623        }
1624        assert_eq!(reader.last(), PadState::NONE, "unplugging releases");
1625    }
1626}