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

psx_io/cd/
reader.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Polled CD-ROM data-sector reads: seek, stream and pop 2048-byte sectors.
3//!
4//! [`SectorReader`] holds the [`Cd`] token and drives the controller through
5//! its register steps, so the register map, the IRQ acknowledge order and the
6//! BCD arithmetic exist once, in [`super`]. What it adds is the command
7//! sequence the pack loader and the FMV streamer have run on silicon: a
8//! SetMode-then-Setloc-then-ReadN start with every IRQ masked and every FIFO
9//! in a known state, a wait loop that drains stale responses (and a stale
10//! data sector into a bounce buffer) before it acknowledges them, and a Pause
11//! that leaves the drive spun up. The comments that record silicon findings
12//! travel with the code.
13//!
14//! Sectors come out of the data FIFO by CPU reads (PIO), not DMA channel 3.
15//! The CL1/CL2 silicon probes convicted the chopping-burst DMA path on real
16//! hardware: the transfer is a state-dependent lottery, and the channel can
17//! latch its start bit and stay busy forever while moving nothing, which read
18//! as all-zero sectors. PIO is the recipe the same probes proved byte-perfect:
19//! arm the data request, wait until the FIFO reports data, pop all 2048 bytes.
20//! It costs about 1.3 ms more per sector than a working DMA, and it cannot
21//! wedge the DMA controller.
22//!
23//! # What `prepare` does to the machine
24//!
25//! [`SectorReader::prepare`] polls the controller's own IRQ flags, so it
26//! keeps CD-ROM interrupts away from the CPU for the length of a stream:
27//!
28//! * `I_MASK` is set to VBlank-only (by `prepare`, and again by each
29//!   `start_read`) until [`SectorReader::stop`] puts the previous value back. Every other source stops reaching the CPU in
30//!   between: a CD-ROM IRQ with no handler would otherwise be an unhandled-IRQ
31//!   storm.
32//! * The latched CD-ROM `I_STAT` bit is acked and the controller's five IRQ
33//!   enables are switched on, then every pending controller IRQ is acked.
34//! * On the first `prepare` of a reader, IRQs latched by earlier activity (the
35//!   BIOS disc boot's file load) are drained. Pausing instead would hang on
36//!   an emulator that has no active read to pause.
37//! * `SetMode` is sent: double speed and 2048-byte user data by default.
38//!
39//! No DMA channel is enabled: nothing here uses one.
40
41use super::lba_to_bcd_msf;
42use crate::irq;
43use crate::periph::Cd;
44use psx_hw::cd::irq::{
45    ACKNOWLEDGE as IRQ_ACK, COMPLETE as IRQ_COMPLETE, DATA_END as IRQ_DATA_END,
46    DATA_READY as IRQ_DATA_READY, ERROR as IRQ_ERROR,
47};
48use psx_hw::cd::{
49    CMD_DEMUTE, CMD_PAUSE, CMD_READN, CMD_SEEKL, CMD_SETFILTER, CMD_SETLOC, CMD_SETMODE,
50    MODE_DOUBLE_SPEED,
51};
52
53/// One CD sector's user data in 32-bit words.
54pub const SECTOR_WORDS: usize = 2048 / 4;
55
56/// `SetMode` byte for double-speed 2048-byte sectors.
57const MODE_DATA_DOUBLE_SPEED: u8 = MODE_DOUBLE_SPEED;
58
59// Poll limits, tuned on silicon. `DATA_POLL` covers a worst-case seek at
60// double speed.
61const ACK_POLL: u32 = 16_384;
62const PARAM_POLL: u32 = 16_384;
63const DATA_POLL: u32 = 4_000_000;
64const CLEANUP_POLL: u32 = 16_384;
65/// Response bytes drained at most. The FIFO holds 16, but on heavy streaming
66/// the controller (or an emulator) can wedge it "not empty", and an unbounded
67/// drain would hang the loader.
68const DRAIN_LIMIT: u32 = 256;
69
70/// Every controller IRQ enable, which `prepare` and a started read switch on.
71const ALL_IRQS_ENABLED: u8 = psx_hw::cd::irq::ALL;
72
73/// [`SectorReader::diagnostics`] cause byte: the drive raised INT5; the
74/// snapshot carries the error response's status and error-code bytes.
75pub const DIAG_CD_ERROR: u8 = 0x05;
76/// [`SectorReader::diagnostics`] cause byte: the wait spun out; the snapshot
77/// carries the raw status register and the last IRQ flag seen.
78pub const DIAG_TIMEOUT: u8 = 0xFF;
79/// [`SectorReader::diagnostics`] cause byte: the parameter FIFO never freed up.
80pub const DIAG_PARAM_STUCK: u8 = 0xFE;
81/// [`SectorReader::diagnostics`] command byte standing in for a `read_sector`
82/// wait (ReadN is streaming; no command byte is in flight).
83pub const DIAG_SITE_READ: u8 = 0xD0;
84
85enum Wait {
86    Matched,
87    CdError,
88    Timeout,
89}
90
91/// The interrupt mask a stream replaced, kept until it ends.
92#[derive(Debug, Default)]
93struct SavedMask(Option<u32>);
94
95impl SavedMask {
96    /// Remember `current`, unless a mask is already kept: a second `prepare`
97    /// before `stop` sees the stream's own VBlank-only mask, which is not the
98    /// caller's.
99    fn keep(&mut self, current: u32) {
100        if self.0.is_none() {
101            self.0 = Some(current);
102        }
103    }
104
105    fn take(&mut self) -> Option<u32> {
106        self.0.take()
107    }
108}
109
110/// Blocking, polled CD-ROM sector reader.
111///
112/// Owns the CD token, plus the state a polled stream needs: whether the
113/// first `prepare` has drained boot-time IRQs, the interrupt mask to put back
114/// at `stop`, and a one-sector bounce buffer that an unexpected data-ready IRQ
115/// is drained into (the data FIFO must be emptied before the ack or the
116/// controller wedges). Create one reader and reuse it for the program's
117/// lifetime; a fresh reader merely repeats the harmless first-time drain.
118///
119/// Typical use is not these raw methods but `psx_pack::cd::load_chunk` on top.
120/// The raw sequence is: `prepare()`, `start_read(lba)`, N times
121/// `read_sector(&mut buf)`, `stop()`.
122///
123/// The reader owns the drive, so nothing else can command it meanwhile:
124///
125/// ```compile_fail,E0382
126/// use psx_io::cd::reader::SectorReader;
127/// use psx_io::periph::Cd;
128///
129/// // SAFETY: the doctest never runs a command; it only has to compile.
130/// let mut cd = unsafe { Cd::steal() };
131/// let mut reader = SectorReader::with_cd(cd);
132/// // The token moved into the reader, so this is a use after move.
133/// let _ = cd.play_track(2);
134/// reader.stop();
135/// ```
136pub struct SectorReader {
137    cd: Cd,
138    prepared: bool,
139    /// Mode byte the last prepare() set; re-sent inside every BIOS-bracket
140    /// read start, because that is what the BIOS does.
141    mode: u8,
142    saved_mask: SavedMask,
143    discard: [u32; SECTOR_WORDS],
144    /// Last failure snapshot: `[cause, status, command, flag-or-error]`.
145    /// Written on every failure path so a caller with only a screen to
146    /// print on (the demo-disc loader) can say what the drive did.
147    diag: [u8; 4],
148}
149
150impl SectorReader {
151    /// A reader that has not yet drained boot-time IRQs. `const` so it can
152    /// sit in a `static`.
153    pub const fn with_cd(cd: Cd) -> Self {
154        SectorReader {
155            cd,
156            prepared: false,
157            mode: MODE_DATA_DOUBLE_SPEED,
158            saved_mask: SavedMask(None),
159            discard: [0; SECTOR_WORDS],
160            diag: [0; 4],
161        }
162    }
163
164    /// [`with_cd`](Self::with_cd) on a token the caller does not hold.
165    #[deprecated(note = "use `SectorReader::with_cd` with the `Cd` token")]
166    pub const fn new() -> Self {
167        // SAFETY: a token is a logic guard, not a memory-safety one (see
168        // `crate::periph`), and the old constructor never took one.
169        Self::with_cd(unsafe { Cd::steal() })
170    }
171
172    /// Give the token back, restoring the interrupt mask first if a stream
173    /// is still open. Call [`stop`](Self::stop) first to also pause the drive.
174    pub fn release(mut self) -> Cd {
175        self.restore_irq_mask();
176        self.cd
177    }
178
179    /// The token, lent for commands outside the reader's own sequences (the
180    /// drive's audio mixer, a status poll). The reader stays borrowed meanwhile.
181    pub fn cd_mut(&mut self) -> &mut Cd {
182        &mut self.cd
183    }
184
185    /// The last failure snapshot packed big-endian:
186    /// `cause<<24 | status<<16 | command<<8 | flag_or_error`. Zero when
187    /// nothing has failed yet. Causes are the `DIAG_*` constants; for
188    /// [`DIAG_CD_ERROR`] the status/flag bytes are the INT5 response pair,
189    /// otherwise status is the raw status register at failure and the low
190    /// byte is the last IRQ flag seen.
191    pub fn diagnostics(&self) -> u32 {
192        u32::from_be_bytes(self.diag)
193    }
194
195    /// Pop one sector's 2048 bytes from the data FIFO into `buffer`.
196    ///
197    /// Arms the data request, waits (bounded) until the FIFO reports data,
198    /// then pops byte by byte. The words are written with volatile stores, in
199    /// order, so the pops and the stores interleave as on silicon.
200    fn pop_sector(cd: &mut Cd, buffer: &mut [u32; SECTOR_WORDS]) {
201        cd.request_data();
202        // The FIFO fills shortly after the request; the bound covers a slow
203        // drive without letting a dead one hang the caller.
204        let mut spins = 0;
205        while !cd.is_data_fifo_ready() && spins < DATA_POLL {
206            spins += 1;
207        }
208        for word in buffer.iter_mut() {
209            let b0 = cd.read_data_byte() as u32;
210            let b1 = cd.read_data_byte() as u32;
211            let b2 = cd.read_data_byte() as u32;
212            let b3 = cd.read_data_byte() as u32;
213            // SAFETY: `word` is a live, aligned `&mut u32` into `buffer`.
214            unsafe { core::ptr::write_volatile(word, (b3 << 24) | (b2 << 16) | (b1 << 8) | b0) };
215        }
216    }
217
218    /// Clear an IRQ we were not waiting for. A stale `DataReady` must have its
219    /// sector popped (into the reader's bounce buffer) before the ack, or the
220    /// data FIFO stays occupied and later reads misalign.
221    fn ack_unexpected(&mut self, flag: u8) {
222        match flag {
223            IRQ_DATA_READY => {
224                Self::pop_sector(&mut self.cd, &mut self.discard);
225                self.cd.drain_response_limited(DRAIN_LIMIT);
226                self.cd.acknowledge_irq(IRQ_DATA_READY);
227            }
228            IRQ_COMPLETE | IRQ_ACK | IRQ_DATA_END => {
229                self.cd.drain_response_limited(DRAIN_LIMIT);
230                self.cd.acknowledge_irq(flag);
231            }
232            _ => {
233                self.cd.drain_response_limited(DRAIN_LIMIT);
234                self.cd.acknowledge_all_and_reset_parameters();
235            }
236        }
237    }
238
239    fn wait_irq(&mut self, expected: u8, limit: u32) -> Wait {
240        let mut spins = 0;
241        while spins < limit {
242            let flag = self.cd.irq_flag_value();
243            if flag == expected {
244                return Wait::Matched;
245            }
246            // Some drives raise the data FIFO before (or without) latching the
247            // DataReady flag; treat visible data as a match.
248            if expected == IRQ_DATA_READY && self.cd.is_data_fifo_ready() {
249                return Wait::Matched;
250            }
251            if flag == IRQ_ERROR {
252                return Wait::CdError;
253            }
254            if flag != 0 {
255                self.ack_unexpected(flag);
256            }
257            spins += 1;
258        }
259        Wait::Timeout
260    }
261
262    /// Record an INT5: its response pair (status, error code) is read before
263    /// the drain throws it away.
264    fn note_drive_error(&mut self, command: u8) {
265        let status = self.cd.read_response_byte();
266        let code = self.cd.read_response_byte();
267        self.diag = [DIAG_CD_ERROR, status, command, code];
268    }
269
270    /// Record a wait that spun out.
271    fn note_timeout(&mut self, command: u8) {
272        let status = self.cd.status_register();
273        let flag = self.cd.irq_flag_value();
274        self.diag = [DIAG_TIMEOUT, status, command, flag];
275    }
276
277    /// Dispatch one command with controller IRQs masked and every FIFO in a
278    /// known state, then wait for `expected` and ack it. The mask/ack ordering
279    /// is load-bearing on silicon; do not reorder.
280    fn send_command(&mut self, command: u8, params: &[u8], expected: u8, limit: u32) -> bool {
281        let saved = self.cd.irq_enable_mask();
282        self.cd.set_irq_enable_mask(0);
283        self.cd.acknowledge_all_and_reset_parameters();
284        self.cd.drain_response_limited(DRAIN_LIMIT);
285        // Reset the parameter FIFO before queueing parameters.
286        self.cd.reset_parameter_fifo();
287        for &param in params {
288            if !self.cd.wait_parameter_room(PARAM_POLL) {
289                self.diag = [DIAG_PARAM_STUCK, self.cd.status_register(), command, 0];
290                self.cd.set_irq_enable_mask(saved);
291                return false;
292            }
293            self.cd.send_parameter_byte(param);
294        }
295        self.cd.send_command_byte(command);
296        let ok = match self.wait_irq(expected, limit) {
297            Wait::Matched => {
298                self.cd.drain_response_limited(DRAIN_LIMIT);
299                self.cd.acknowledge_irq(expected);
300                true
301            }
302            Wait::CdError => {
303                self.note_drive_error(command);
304                self.cd.drain_response_limited(DRAIN_LIMIT);
305                self.cd.acknowledge_all_and_reset_parameters();
306                false
307            }
308            Wait::Timeout => {
309                self.note_timeout(command);
310                false
311            }
312        };
313        self.cd.set_irq_enable_mask(saved);
314        ok
315    }
316
317    /// Set `I_MASK` to VBlank-only, keeping the caller's mask for
318    /// [`stop`](Self::stop). Idempotent within a stream.
319    fn enter_polling_mask(&mut self) {
320        self.saved_mask.keep(irq::mask());
321        irq::set_mask(1 << psx_hw::irq::source::VBLANK);
322    }
323
324    fn restore_irq_mask(&mut self) {
325        if let Some(mask) = self.saved_mask.take() {
326            irq::set_mask(mask);
327        }
328    }
329
330    /// Take over the controller for polled data reads and set
331    /// double-speed / 2048-byte-sector mode.
332    ///
333    /// Sets `I_MASK` to VBlank-only until [`stop`](Self::stop) puts the
334    /// previous mask back; see the module docs for the full list of side
335    /// effects. Call it only from code that owns interrupt policy, a polling
336    /// main loop, and not from an IRQ handler. No CD-ROM IRQ handler may be
337    /// installed while a stream runs.
338    ///
339    /// Returns `false` when the Setmode handshake times out (no drive, tray
340    /// open, dead controller); the reader is safe to retry.
341    pub fn prepare(&mut self) -> bool {
342        self.prepare_with_mode(MODE_DATA_DOUBLE_SPEED)
343    }
344
345    /// [`prepare`](Self::prepare) at single speed: half the throughput,
346    /// twice the per-sector margin. The demo-disc chain loader measured
347    /// silent payload corruption over hundreds of back-to-back
348    /// double-speed sectors on the project console (2026-08-01, the
349    /// loader's RAM checksum against the disc build); the header sector
350    /// alone always read clean, so the failure scales with sustained
351    /// rate, and a loader that takes three extra seconds beats one that
352    /// jumps into a corrupt payload.
353    pub fn prepare_single_speed(&mut self) -> bool {
354        self.prepare_with_mode(0x00)
355    }
356
357    /// [`prepare`](Self::prepare) with an explicit Setmode byte, for
358    /// streams that need more than plain data: e.g. `0x80 | 0x40 | 0x08`
359    /// (double speed, XA-ADPCM on, file/channel filter) plays interleaved
360    /// XA audio through the SPU while video sectors still arrive as data.
361    pub fn prepare_mode(&mut self, mode: u8) -> bool {
362        self.prepare_with_mode(mode)
363    }
364
365    /// Setfilter: the XA file and channel whose audio sectors the drive
366    /// plays when the mode has the filter bit set.
367    pub fn set_filter(&mut self, file: u8, channel: u8) -> bool {
368        self.send_command(CMD_SETFILTER, &[file, channel], IRQ_ACK, ACK_POLL)
369    }
370
371    /// Unmute the drive: let CD-DA and XA-ADPCM reach the SPU. The drive
372    /// stays muted across programs, so a stream that plays XA audio must not
373    /// assume the last tenant left it unmuted.
374    #[doc(alias = "Demute")]
375    pub fn unmute(&mut self) -> bool {
376        self.send_command(CMD_DEMUTE, &[], IRQ_ACK, ACK_POLL)
377    }
378
379    fn prepare_with_mode(&mut self, mode: u8) -> bool {
380        self.mode = mode;
381        // Keep CD-ROM at the controller level and poll its IRQ flags
382        // manually, so DataReady cannot enter an unhandled CPU IRQ storm.
383        self.enter_polling_mask();
384        irq::acknowledge(1 << psx_hw::irq::source::CDROM);
385        self.cd.set_irq_enable_mask(ALL_IRQS_ENABLED);
386        self.cd.acknowledge_all_and_reset_parameters();
387        if !self.prepared {
388            // A BIOS disc boot has already finished its file load. Do not
389            // send Pause before our first stream; on real BIOS boot paths
390            // some emulators have no active read command to pause and
391            // never acknowledge it. Drain any already-latched data/ack
392            // instead.
393            let mut drained = 0;
394            while drained < 16 {
395                let flag = self.cd.irq_flag_value();
396                if flag == 0 {
397                    break;
398                }
399                self.ack_unexpected(flag);
400                drained += 1;
401            }
402            self.cd.acknowledge_all_and_reset_parameters();
403            self.prepared = true;
404        }
405        // Purge whatever the previous tenant left in the data FIFO by
406        // dropping the data request (writing 0 resets the data FIFO). The
407        // demo-disc chain loader's header read came back with shifted bytes
408        // on silicon (magic mismatch, identical every attempt) after the
409        // menu's earlier disc reads; leftover FIFO bytes are the only state
410        // that survives the IRQ drain above, and an emulator FIFO never holds
411        // any, which is why this cannot reproduce headless.
412        self.cd.clear_data_request();
413        self.send_command(CMD_SETMODE, &[mode], IRQ_ACK, ACK_POLL)
414    }
415
416    /// Setloc to a program-relative `lba`, shifted by [`crate::disc_base`]
417    /// to where this program's image actually landed on a multi-program disc.
418    fn set_location(&mut self, lba: u32) -> bool {
419        let [minute, second, frame] = lba_to_bcd_msf(crate::disc_base::shift_lba(lba));
420        self.send_command(CMD_SETLOC, &[minute, second, frame], IRQ_ACK, ACK_POLL)
421    }
422
423    /// Seek to `lba` (Setloc with BCD MSF) and start a ReadN stream.
424    /// [`prepare`](Self::prepare) must have succeeded first.
425    ///
426    /// `lba` is relative to the start of this program's own disc image; on a
427    /// multi-program disc [`crate::disc_base`] shifts it to where that image
428    /// actually landed.
429    pub fn start_read(&mut self, lba: u32) -> bool {
430        // A caller that prepared once and starts several streams still gets
431        // the polling mask for each, and `stop` still puts its own back.
432        self.enter_polling_mask();
433        if !self.set_location(lba) {
434            return false;
435        }
436        if !self.send_command(CMD_READN, &[], IRQ_ACK, ACK_POLL) {
437            return false;
438        }
439        self.cd.set_irq_enable_mask(ALL_IRQS_ENABLED);
440        true
441    }
442
443    /// [`start_read`](Self::start_read) the way the real BIOS starts one:
444    /// SetLoc, then an EXPLICIT SeekL waited to completion, then ReadN.
445    ///
446    /// Traced from a real SCPH-1001 boot (2026-08-01, emulator CD command
447    /// log): the BIOS brackets every read -- even a single sector -- as
448    /// SetLoc/SeekL/SetMode/ReadN/Pause. The implicit seek a bare
449    /// SetLoc+ReadN performs starts data flowing while the mech is still
450    /// settling; the same console that corrupts our sustained implicit-seek
451    /// streams loads 1.4 MB EXEs through the BIOS bracket without fault.
452    pub fn start_read_seek_first(&mut self, lba: u32, seek_poll: u32) -> bool {
453        self.enter_polling_mask();
454        if !self.set_location(lba) {
455            return false;
456        }
457        // SeekL acks (INT3) then completes (INT2) once the head has
458        // settled on the target; only then is ReadN issued, so the
459        // drive never streams during mech settle.
460        if !self.send_command(CMD_SEEKL, &[], IRQ_ACK, ACK_POLL) {
461            return false;
462        }
463        match self.wait_irq(IRQ_COMPLETE, seek_poll) {
464            Wait::Matched => {}
465            Wait::CdError | Wait::Timeout => return false,
466        }
467        self.cd.acknowledge_irq(IRQ_COMPLETE);
468        // The BIOS re-sends SetMode inside every bracket, between the
469        // seek completion and ReadN; every one of its ReadN commands in
470        // the trace is prefixed SetLoc,SeekL,SetMode. Match it exactly.
471        let mode = self.mode;
472        if !self.send_command(CMD_SETMODE, &[mode], IRQ_ACK, ACK_POLL) {
473            return false;
474        }
475        if !self.send_command(CMD_READN, &[], IRQ_ACK, ACK_POLL) {
476            return false;
477        }
478        self.cd.set_irq_enable_mask(ALL_IRQS_ENABLED);
479        true
480    }
481
482    /// Block until the next sector of the running ReadN stream is ready, then
483    /// pop its 2048 bytes into `buffer`. `false` on drive error or timeout
484    /// (the stream is acked/cleaned up; follow with [`stop`](Self::stop)). A
485    /// read must be running (a successful [`start_read`](Self::start_read)).
486    pub fn read_sector(&mut self, buffer: &mut [u32; SECTOR_WORDS]) -> bool {
487        match self.wait_irq(IRQ_DATA_READY, DATA_POLL) {
488            Wait::Matched => {}
489            Wait::CdError => {
490                self.note_drive_error(DIAG_SITE_READ);
491                self.cd.drain_response_limited(DRAIN_LIMIT);
492                self.cd.acknowledge_all_and_reset_parameters();
493                return false;
494            }
495            Wait::Timeout => {
496                self.note_timeout(DIAG_SITE_READ);
497                self.cd.drain_response_limited(DRAIN_LIMIT);
498                self.cd.acknowledge_all_and_reset_parameters();
499                return false;
500            }
501        }
502        Self::pop_sector(&mut self.cd, buffer);
503        self.cd.drain_response_limited(DRAIN_LIMIT);
504        self.cd.acknowledge_irq(IRQ_DATA_READY);
505        true
506    }
507
508    /// Non-blocking [`read_sector`](Self::read_sector): check the controller
509    /// once and, if the next sector of the running ReadN stream is ready,
510    /// pop its 2048 bytes into `buffer` and ack it.
511    ///
512    /// `Ok(true)` means `buffer` holds a new sector, `Ok(false)` that none
513    /// has arrived yet. `Err(())` is a drive error; the stream is acked and
514    /// the caller should [`stop`](Self::stop) (the diag snapshot is set as
515    /// for `read_sector`).
516    ///
517    /// Streaming consumers (FMV) call this between units of other work so
518    /// the drive never runs ahead of the CPU by more than the controller can
519    /// hold. At double speed a sector lands every ~6.7 ms.
520    #[allow(
521        clippy::result_unit_err,
522        reason = "the failure detail is in `diagnostics`; callers only branch on it"
523    )]
524    pub fn try_read_sector(&mut self, buffer: &mut [u32; SECTOR_WORDS]) -> Result<bool, ()> {
525        let flag = self.cd.irq_flag_value();
526        if flag == IRQ_ERROR {
527            self.note_drive_error(DIAG_SITE_READ);
528            self.cd.drain_response_limited(DRAIN_LIMIT);
529            self.cd.acknowledge_all_and_reset_parameters();
530            return Err(());
531        }
532        if flag != IRQ_DATA_READY && !self.cd.is_data_fifo_ready() {
533            if flag != 0 {
534                self.ack_unexpected(flag);
535            }
536            return Ok(false);
537        }
538        Self::pop_sector(&mut self.cd, buffer);
539        self.cd.drain_response_limited(DRAIN_LIMIT);
540        self.cd.acknowledge_irq(IRQ_DATA_READY);
541        Ok(true)
542    }
543
544    /// Pause the ReadN stream (keeps the drive spun up), ack everything and
545    /// put the interrupt mask [`prepare`](Self::prepare) replaced back.
546    /// Safe to call after failures; it tolerates a drive with no active read.
547    pub fn stop(&mut self) {
548        if self.send_command(CMD_PAUSE, &[], IRQ_ACK, CLEANUP_POLL) {
549            let _ = self.wait_irq(IRQ_COMPLETE, CLEANUP_POLL);
550            self.cd.drain_response_limited(DRAIN_LIMIT);
551            self.cd.acknowledge_irq(IRQ_COMPLETE);
552        }
553        self.cd.acknowledge_all_and_reset_parameters();
554        // Last, so a CD-ROM IRQ still latched cannot reach the CPU through
555        // the restored mask.
556        self.restore_irq_mask();
557    }
558}
559
560#[allow(
561    deprecated,
562    reason = "the old no-argument constructor is what Default always meant"
563)]
564impl Default for SectorReader {
565    fn default() -> Self {
566        Self::new()
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn the_reader_owns_the_token_and_hands_it_back() {
576        // SAFETY: a test-local token on the host. Releasing a reader that never
577        // prepared touches no register (no mask is saved to restore).
578        let reader = SectorReader::with_cd(unsafe { Cd::steal() });
579        assert_eq!(reader.diagnostics(), 0);
580        let _token: Cd = reader.release();
581    }
582
583    #[test]
584    fn a_reader_fits_a_static_because_the_constructor_is_const() {
585        // SAFETY: a test-local token on the host; the static is never used.
586        static _READER: SectorReader = SectorReader::with_cd(unsafe { Cd::steal() });
587    }
588
589    #[test]
590    fn a_second_prepare_keeps_the_callers_mask_not_the_streams() {
591        let mut saved = SavedMask::default();
592        saved.keep(0x0000_0305); // the caller's mask
593        saved.keep(1 << psx_hw::irq::source::VBLANK); // a second prepare's view
594        assert_eq!(saved.take(), Some(0x0000_0305));
595        assert_eq!(saved.take(), None, "stop restores once");
596        saved.keep(0x42);
597        assert_eq!(saved.take(), Some(0x42), "a new stream saves afresh");
598    }
599
600    #[test]
601    fn the_sector_is_2048_bytes_of_words() {
602        assert_eq!(SECTOR_WORDS * 4, 2048);
603    }
604}