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 ¶m 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}