psx_cdstream/engine.rs
1//! The transport state machine ([`Engine`]) and its configuration and counters.
2
3use crate::hw::CdHw;
4use crate::request::{
5 Completion, Failure, Outcome, Priority, Request, RequestState, SubmitError, Ticket,
6 SECTOR_WORDS,
7};
8use psx_hw::cd::{
9 irq as flag, CMD_PAUSE, CMD_READN, CMD_SEEKL, CMD_SETLOC, CMD_SETMODE, MODE_DOUBLE_SPEED,
10};
11
12/// Requests that can wait behind the active one.
13pub const QUEUE_DEPTH: usize = 4;
14/// Finished requests whose results [`Engine::state`] still remembers.
15pub const RECENT_RESULTS: usize = 8;
16
17/// Where the controller is in a transfer. The numbers are published in
18/// `PSX_CD_STATS` and match the earlier game-local transports.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[repr(u32)]
21pub enum Phase {
22 /// Nothing has run since the engine was created.
23 Idle = 0,
24 /// Setloc sent, waiting for its acknowledge.
25 SettingTarget = 1,
26 /// SeekL sent, waiting for its acknowledge.
27 StartingSeek = 2,
28 /// Seek acknowledged, waiting for it to complete.
29 Seeking = 3,
30 /// Setmode sent, waiting for its acknowledge.
31 SettingMode = 4,
32 /// ReadN sent, waiting for its acknowledge. Sectors may already arrive.
33 StartingRead = 5,
34 /// Sectors are arriving.
35 Reading = 6,
36 /// Pause sent, waiting for its acknowledge.
37 Stopping = 7,
38 /// Pause acknowledged, waiting for the drive to stop.
39 WaitingForStop = 8,
40 /// The drive is stopped and the last transfer ended cleanly.
41 Done = 9,
42 /// A transfer failed; the drive's state is unknown. The next transfer
43 /// starts with a recovery Pause.
44 Failed = 10,
45}
46
47impl Phase {
48 const fn is_terminal(self) -> bool {
49 matches!(self, Phase::Idle | Phase::Done | Phase::Failed)
50 }
51}
52
53/// Who may program the controller.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum Owner {
56 /// Nobody has attached the transport yet, or it was detached: the
57 /// program uses the blocking `SectorReader` or drives the controller
58 /// itself, and no request starts.
59 Boot,
60 /// The transport reads. Requests run.
61 Data,
62 /// CD-DA or XA playback holds the drive. Requests queue but do not start.
63 Audio,
64}
65
66/// Where an audio lease request stands.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum LeaseState {
69 /// No lease is held or wanted.
70 None,
71 /// Asked for; the in-flight request is being stopped.
72 Pending,
73 /// The drive is the audio code's. The transport masks its interrupt
74 /// source and starts nothing until the lease is released.
75 Granted,
76}
77
78/// Whether the CD interrupt source stays open between transfers.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum IrqMaskPolicy {
81 /// Open from attach until detach or an audio lease.
82 AlwaysOn,
83 /// Open only while a request is queued or active, so polled code
84 /// (the SDK's `SectorReader`) never shares the controller with the
85 /// handler between transfers.
86 OnlyWhileBusy,
87}
88
89/// How the engine behaves.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub struct Config {
92 /// A transfer with no sector, response or phase change for this many
93 /// VBlanks fails with [`FailureKind::Watchdog`](crate::FailureKind).
94 pub timeout_vblanks: u32,
95 /// See [`IrqMaskPolicy`].
96 pub irq_mask_policy: IrqMaskPolicy,
97 /// Start the first transfer after an audio lease with a recovery Pause,
98 /// so a drive that was playing or parked at an audio position is stopped
99 /// before the seek. Costs one command; skip it only if the audio code
100 /// always ends with its own Pause.
101 pub pause_after_audio: bool,
102 /// Measure how long each handler call takes, with Timer 2, which the
103 /// transport then owns. The longest call is `max_irq_ticks`, in
104 /// system-clock / 8 ticks that wrap after about 15.5 ms.
105 pub time_handler: bool,
106 /// Read at double speed (150 sectors per second, the default and the
107 /// only rate a streamer wants). Clear it for single speed (75 sectors
108 /// per second), which halves the CPU the sector pops cost per second of
109 /// reading; it applies from the next transfer.
110 pub double_speed: bool,
111}
112
113impl Config {
114 /// The defaults: 600 VBlanks, interrupt source always on, recovery pause
115 /// after audio, no handler timing, double speed.
116 pub const DEFAULT: Config = Config {
117 timeout_vblanks: 600,
118 irq_mask_policy: IrqMaskPolicy::AlwaysOn,
119 pause_after_audio: true,
120 time_handler: false,
121 double_speed: true,
122 };
123}
124
125impl Default for Config {
126 fn default() -> Self {
127 Self::DEFAULT
128 }
129}
130
131/// Counters, published as one block (`PSX_CD_STATS`) for tools that read
132/// guest memory. All wrap at 2^32.
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
134#[repr(C)]
135pub struct StreamStats {
136 /// Interrupts handled.
137 pub irq_count: u32,
138 /// Sectors stored at a destination.
139 pub sectors: u32,
140 /// Sectors popped and dropped: after a cancel, after the last sector of
141 /// a request while the Pause was on its way, or with nothing to store to.
142 pub discarded_sectors: u32,
143 /// The longest handler call, in Timer 2 ticks (needs
144 /// [`Config::time_handler`]).
145 pub max_irq_ticks: u32,
146 /// The current [`Phase`], as its number.
147 pub phase: u32,
148 /// The code of the latest failure ([`Failure::code`]); 0 after a start.
149 pub error: u32,
150 /// Requests that ended [`Outcome::Done`].
151 pub requests_done: u32,
152 /// Requests that ended [`Outcome::Cancelled`].
153 pub requests_cancelled: u32,
154 /// Requests that ended [`Outcome::Failed`].
155 pub requests_failed: u32,
156 /// Requests started by chaining onto the previous one, without a seek.
157 pub chained: u32,
158 /// Requests put back in the queue to resume after an error.
159 pub resumed: u32,
160 /// Submissions refused because the queue was full.
161 pub rejected: u32,
162}
163
164#[derive(Clone, Copy)]
165struct Active {
166 ticket: Ticket,
167 request: Request,
168 /// Sectors a request resumed from an error had stored before this
169 /// activation; `request` is only the rest.
170 base: u32,
171 received: u32,
172 cancelled: bool,
173 resumes_left: u8,
174}
175
176#[derive(Clone, Copy)]
177struct Queued {
178 ticket: Ticket,
179 request: Request,
180 /// Sectors already stored by earlier activations (see `Active::base`).
181 base: u32,
182 /// Submission order; resumed requests carry 0 so they go first within
183 /// their priority.
184 seq: u32,
185 resumes_left: u8,
186}
187
188/// The CD transport.
189///
190/// One engine owns the drive. Foreground code submits [`Request`]s; the CD
191/// interrupt handler calls [`Engine::on_interrupt`] once per interrupt, and
192/// each call advances the machine by one controller event. Nothing in the
193/// machine waits for the drive: a command is written and the machine returns,
194/// and the response arrives as the next interrupt.
195///
196/// # A transfer
197///
198/// ```text
199/// Setloc ──INT3──▶ SeekL ──INT3──▶ (seek) ──INT2──▶ Setmode(double speed)
200/// ──INT3──▶ ReadN ──INT3──▶ INT1 × n (one sector each) ──▶ Pause
201/// ──INT3──▶ (stop) ──INT2──▶ stopped
202/// ```
203///
204/// The read is seek-first (Setloc, SeekL, then Setmode and ReadN), the form
205/// the BIOS uses and the only one proven to stream byte-perfect on a console:
206/// a bare Setloc plus ReadN starts delivering while the mechanism is still
207/// settling. Each INT1 pops one sector into the request's destination. After
208/// a failure the drive's state is unknown, so the next transfer opens with a
209/// recovery Pause.
210///
211/// # Chaining
212///
213/// When the last sector of a request lands and the best queued request starts
214/// at the next sector, the machine swaps the destination inside that same
215/// interrupt and keeps reading: no Pause, no seek. A request is `Done` the
216/// moment its last sector lands, even though the drive is still running on
217/// to the Pause; [`Engine::is_idle`] says when it has stopped.
218///
219/// # Abort
220///
221/// [`Engine::cancel`] only sets a flag. The handler drops the next sector it
222/// pops and issues Pause itself. Pausing from the foreground while sectors
223/// keep arriving lost the Pause acknowledge on silicon (no interrupt for 600
224/// VBlanks), so the foreground never does it.
225///
226/// # Generic over the drive
227///
228/// On the console the one instance lives in the crate's global and is reached
229/// through the free functions at the crate root; on the host a scripted drive
230/// stands in.
231pub struct Engine<H: CdHw> {
232 hw: H,
233 config: Config,
234 owner: Owner,
235 lease_pending: bool,
236 /// Whether the CPU-side CD source is currently open, as last written.
237 source_open: bool,
238 /// The first transfer starts with a recovery Pause.
239 recover_next: bool,
240 phase: Phase,
241 /// Set while the Pause that opens a transfer after a failure is running.
242 recovering: bool,
243 error: u32,
244 /// VBlank count at the last sign of life.
245 stamp: u32,
246 /// Absolute BCD Setloc parameters of the active request.
247 location: [u8; 3],
248 active: Option<Active>,
249 /// One spare slot beyond [`QUEUE_DEPTH`]: a failing request that resumes
250 /// goes back in the queue even when it is full.
251 queue: [Option<Queued>; QUEUE_DEPTH + 1],
252 results: [Option<Completion>; RECENT_RESULTS],
253 results_written: u32,
254 next_ticket: u32,
255 next_seq: u32,
256 stats: StreamStats,
257}
258
259impl<H: CdHw> Engine<H> {
260 /// An engine in the [`Owner::Boot`] state: nothing runs until
261 /// [`attach`](Self::attach).
262 pub const fn new(hw: H, config: Config) -> Self {
263 Engine {
264 hw,
265 config,
266 owner: Owner::Boot,
267 lease_pending: false,
268 source_open: false,
269 recover_next: false,
270 phase: Phase::Idle,
271 recovering: false,
272 error: 0,
273 stamp: 0,
274 location: [0; 3],
275 active: None,
276 queue: [None; QUEUE_DEPTH + 1],
277 results: [None; RECENT_RESULTS],
278 results_written: 0,
279 next_ticket: 1,
280 next_seq: 1,
281 stats: StreamStats {
282 irq_count: 0,
283 sectors: 0,
284 discarded_sectors: 0,
285 max_irq_ticks: 0,
286 phase: 0,
287 error: 0,
288 requests_done: 0,
289 requests_cancelled: 0,
290 requests_failed: 0,
291 chained: 0,
292 resumed: 0,
293 rejected: 0,
294 },
295 }
296 }
297
298 /// Replace the configuration. It applies from the next event.
299 pub fn configure(&mut self, config: Config) {
300 self.config = config;
301 }
302
303 /// The drive layer.
304 pub fn hw(&self) -> &H {
305 &self.hw
306 }
307
308 /// The drive layer, mutably.
309 pub fn hw_mut(&mut self) -> &mut H {
310 &mut self.hw
311 }
312
313 /// Who may program the controller.
314 pub fn owner(&self) -> Owner {
315 self.owner
316 }
317
318 /// The controller phase.
319 pub fn phase(&self) -> Phase {
320 self.phase
321 }
322
323 /// A copy of the counters, with the current phase and error filled in.
324 pub fn stats(&self) -> StreamStats {
325 StreamStats {
326 phase: self.phase as u32,
327 error: self.error,
328 ..self.stats
329 }
330 }
331
332 /// Start the longest-handler measurement over
333 /// ([`StreamStats::max_irq_ticks`] to 0), to attribute the peak to the
334 /// next stretch of work.
335 pub fn reset_max_irq_ticks(&mut self) {
336 self.stats.max_irq_ticks = 0;
337 }
338
339 /// Requests waiting behind the active one.
340 pub fn queued_count(&self) -> usize {
341 self.queue.iter().flatten().count()
342 }
343
344 /// Whether the drive is stopped and no request holds it. Queued requests
345 /// may still be waiting for their turn (while the owner is not `Data`).
346 pub fn is_idle(&self) -> bool {
347 self.phase.is_terminal() && self.active.is_none()
348 }
349
350 // ----------------------------------------------------------------- owner
351
352 /// Take the drive: [`Owner::Boot`] becomes [`Owner::Data`] and queued
353 /// requests start.
354 ///
355 /// The controller must be in a clean state: the polled reader released,
356 /// no command in flight. A no-op in any other state.
357 pub fn attach(&mut self) {
358 if self.owner != Owner::Boot {
359 return;
360 }
361 self.owner = Owner::Data;
362 self.recover_next = false;
363 self.hw.discard_response();
364 self.hw.acknowledge(flag::ACK_ALL);
365 self.settle();
366 self.sync_source();
367 }
368
369 /// Hand the drive back to polled code. Succeeds only when it is idle and
370 /// nothing is queued or wanted; [`cancel_all`](Self::cancel_all) and wait
371 /// for [`is_idle`](Self::is_idle) first.
372 pub fn detach(&mut self) -> bool {
373 if self.owner != Owner::Data
374 || !self.is_idle()
375 || self.queued_count() != 0
376 || self.lease_pending
377 {
378 return false;
379 }
380 self.owner = Owner::Boot;
381 self.sync_source();
382 true
383 }
384
385 /// Where an audio lease stands.
386 pub fn lease_state(&self) -> LeaseState {
387 if self.owner == Owner::Audio {
388 LeaseState::Granted
389 } else if self.lease_pending {
390 LeaseState::Pending
391 } else {
392 LeaseState::None
393 }
394 }
395
396 /// Ask for the drive on behalf of CD-DA or XA playback.
397 ///
398 /// New requests are still accepted but queue. A request in flight is
399 /// aborted at its next sector (it ends [`Outcome::Cancelled`] with what
400 /// landed; resume it with [`Request::remaining_after`] after the lease).
401 /// When the drive has stopped the lease is granted: the CD interrupt
402 /// source is masked, and the audio code may program the controller
403 /// directly. Poll [`lease_state`](Self::lease_state) until it reads
404 /// [`LeaseState::Granted`] if this returns `Pending`.
405 ///
406 /// Before [`attach`](Self::attach) the program already holds the drive
407 /// and this returns `Granted` without changing anything.
408 pub fn request_audio_lease(&mut self) -> LeaseState {
409 match self.owner {
410 Owner::Boot => return LeaseState::Granted,
411 Owner::Audio => return LeaseState::Granted,
412 Owner::Data => {}
413 }
414 self.lease_pending = true;
415 if let Some(active) = self.active.as_mut() {
416 active.cancelled = true;
417 }
418 if self.is_idle() {
419 self.grant_lease();
420 }
421 self.sync_source();
422 self.lease_state()
423 }
424
425 /// Take the drive for audio only if it can be had right now, without
426 /// waiting and without leaving anything behind.
427 ///
428 /// Returns `true` when the lease is granted (the drive was idle, or the
429 /// program still holds it before [`attach`](Self::attach)). Returns
430 /// `false` when a request is in flight: nothing changes, no lease is left
431 /// pending and the request is not aborted, so a read submitted afterwards
432 /// cannot end up queued behind a lease that is granted later. Ask again
433 /// when the drive is idle.
434 ///
435 /// [`request_audio_lease`](Self::request_audio_lease) is for audio that
436 /// must displace a read; this is for audio that only wants the drive
437 /// while nobody else is using it.
438 pub fn try_audio_lease(&mut self) -> bool {
439 match self.owner {
440 Owner::Boot | Owner::Audio => true,
441 Owner::Data => {
442 if self.lease_pending || !self.is_idle() {
443 return false;
444 }
445 self.grant_lease();
446 self.sync_source();
447 true
448 }
449 }
450 }
451
452 /// Withdraw a lease that was asked for and not yet granted. Returns
453 /// whether one was pending.
454 ///
455 /// Call it before submitting a read that must not wait for audio: a
456 /// pending lease is granted the moment the drive stops, which parks every
457 /// read queued behind it until the audio code gives the drive back. The
458 /// request that was aborted for the lease still ends
459 /// [`Outcome::Cancelled`] (resume it with [`Request::remaining_after`]).
460 /// A lease that is already granted is ended with
461 /// [`release_audio_lease`](Self::release_audio_lease).
462 pub fn withdraw_audio_lease(&mut self) -> bool {
463 if self.owner == Owner::Data && self.lease_pending {
464 self.lease_pending = false;
465 return true;
466 }
467 false
468 }
469
470 /// Give the drive back after audio playback, or withdraw a lease that is
471 /// still pending. Returns whether there was a lease to end.
472 ///
473 /// The audio code must have ended playback with Pause (never Stop: the
474 /// motor then spins down, reports status 0 for a second or two, and reads
475 /// started in that window failed on a console). The next transfer starts
476 /// with a recovery Pause if [`Config::pause_after_audio`] is set.
477 pub fn release_audio_lease(&mut self) -> bool {
478 if self.withdraw_audio_lease() {
479 return true;
480 }
481 if self.owner != Owner::Audio {
482 return false;
483 }
484 self.hw.discard_response();
485 self.hw.acknowledge(flag::ACK_ALL);
486 self.owner = Owner::Data;
487 self.recover_next = self.config.pause_after_audio;
488 self.settle();
489 self.sync_source();
490 true
491 }
492
493 fn grant_lease(&mut self) {
494 self.owner = Owner::Audio;
495 self.lease_pending = false;
496 }
497
498 // -------------------------------------------------------------- requests
499
500 /// Queue a request. It starts at once when the drive is free.
501 ///
502 /// Higher priority (lower number) goes first; equal priorities go in
503 /// submission order, except that one starting at the sector after the
504 /// active request's last is picked ahead of its peers so it chains
505 /// without a seek.
506 pub fn submit(&mut self, request: Request) -> Result<Ticket, SubmitError> {
507 if request.destination.is_null() {
508 return Err(SubmitError::NullDestination);
509 }
510 if request.destination as usize & 3 != 0 {
511 return Err(SubmitError::MisalignedDestination);
512 }
513 if request.sectors == 0 {
514 return Err(SubmitError::Empty);
515 }
516 if self.queued_count() >= QUEUE_DEPTH {
517 self.stats.rejected = self.stats.rejected.wrapping_add(1);
518 return Err(SubmitError::QueueFull);
519 }
520 let ticket = self.new_ticket();
521 let seq = self.next_seq;
522 self.next_seq = self.next_seq.wrapping_add(1).max(1);
523 self.enqueue(Queued {
524 ticket,
525 request,
526 base: 0,
527 seq,
528 resumes_left: request.resumes,
529 });
530 self.service();
531 // A free drive picks the request up now; a busy one keeps it queued
532 // and `settle` runs when it frees up.
533 self.settle();
534 self.sync_source();
535 Ok(ticket)
536 }
537
538 /// Stop a request. Returns whether `ticket` was queued or active.
539 ///
540 /// A queued request is dropped at once ([`Outcome::Cancelled`], 0
541 /// sectors). The active one is flagged: the handler drops the next
542 /// sector, pauses the drive, and the request ends `Cancelled` with the
543 /// sectors that had landed.
544 pub fn cancel(&mut self, ticket: Ticket) -> bool {
545 if self.active.is_some_and(|active| active.ticket == ticket) {
546 self.cancel_active();
547 return true;
548 }
549 for index in 0..self.queue.len() {
550 if self.queue[index].is_some_and(|queued| queued.ticket == ticket) {
551 self.drop_queued(index);
552 return true;
553 }
554 }
555 false
556 }
557
558 /// [`cancel`](Self::cancel) everything: the queue and the active request.
559 pub fn cancel_all(&mut self) {
560 for index in 0..self.queue.len() {
561 self.drop_queued(index);
562 }
563 if self.active.is_some() {
564 self.cancel_active();
565 }
566 }
567
568 fn cancel_active(&mut self) {
569 self.trace(2, 0);
570 if let Some(active) = self.active.as_mut() {
571 active.cancelled = true;
572 }
573 }
574
575 fn drop_queued(&mut self, index: usize) {
576 if let Some(queued) = self.queue[index].take() {
577 self.record(Completion {
578 ticket: queued.ticket,
579 outcome: Outcome::Cancelled,
580 received: queued.base,
581 });
582 }
583 }
584
585 /// Where `ticket` is. Also runs the no-progress check ([`service`](Self::service)).
586 pub fn state(&mut self, ticket: Ticket) -> RequestState {
587 self.service();
588 self.sync_source();
589 if let Some(active) = &self.active {
590 if active.ticket == ticket {
591 return RequestState::Active {
592 received: active.base + active.received,
593 };
594 }
595 }
596 if self.queue.iter().flatten().any(|q| q.ticket == ticket) {
597 return RequestState::Queued;
598 }
599 if let Some(done) = self.results.iter().flatten().find(|c| c.ticket == ticket) {
600 return RequestState::Finished(*done);
601 }
602 RequestState::Unknown
603 }
604
605 /// Foreground upkeep: fail a transfer that has made no progress for
606 /// [`Config::timeout_vblanks`]. [`state`](Self::state) calls this; a main
607 /// loop that never asks about a ticket should call it once per frame.
608 pub fn service(&mut self) {
609 if self.owner != Owner::Data || self.phase.is_terminal() {
610 return;
611 }
612 let now = self.hw.vblank_count();
613 if now.wrapping_sub(self.stamp) > self.config.timeout_vblanks {
614 self.fail(Failure::watchdog(self.phase as u32));
615 self.sync_source();
616 }
617 }
618
619 // ------------------------------------------------------------- interrupt
620
621 /// Handle one CD interrupt. Called by the interrupt handler (the crate's
622 /// exception wrapper on the console, the test harness on the host).
623 pub fn on_interrupt(&mut self) {
624 let started = self.hw.clock();
625 self.stats.irq_count = self.stats.irq_count.wrapping_add(1);
626 if self.owner == Owner::Data {
627 let code = self.hw.interrupt_code();
628 self.trace(1, code);
629 self.dispatch(code);
630 self.sync_source();
631 }
632 let ticks = u32::from(self.hw.clock().wrapping_sub(started));
633 self.stats.max_irq_ticks = self.stats.max_irq_ticks.max(ticks);
634 }
635
636 fn dispatch(&mut self, code: u8) {
637 if code == flag::DATA_READY {
638 match self.take_sector() {
639 Ok(()) => {
640 self.hw.discard_response();
641 self.hw.acknowledge(code);
642 self.after_sector();
643 }
644 Err(failure) => {
645 self.hw.discard_response();
646 self.hw.acknowledge(flag::ACK_ALL);
647 self.fail(failure);
648 }
649 }
650 return;
651 }
652 if code == flag::ERROR {
653 let (status, detail) = self.hw.error_response();
654 self.hw.discard_response();
655 self.hw.acknowledge(flag::ACK_ALL);
656 self.fail(Failure::drive(status, self.phase as u32, detail));
657 return;
658 }
659 self.hw.discard_response();
660 self.hw.acknowledge(code);
661 match (self.phase, code) {
662 (Phase::SettingTarget, flag::ACKNOWLEDGE) => {
663 self.command(CMD_SEEKL, &[], Phase::StartingSeek)
664 }
665 (Phase::StartingSeek, flag::ACKNOWLEDGE) => self.change(Phase::Seeking),
666 (Phase::Seeking, flag::COMPLETE) => {
667 let mode = if self.config.double_speed {
668 MODE_DOUBLE_SPEED
669 } else {
670 0
671 };
672 self.command(CMD_SETMODE, &[mode], Phase::SettingMode)
673 }
674 (Phase::SettingMode, flag::ACKNOWLEDGE) => {
675 self.command(CMD_READN, &[], Phase::StartingRead)
676 }
677 (Phase::StartingRead, flag::ACKNOWLEDGE) => {
678 self.change(Phase::Reading);
679 if self.active.is_some_and(|a| a.cancelled) {
680 self.pause();
681 }
682 }
683 (Phase::Stopping, flag::ACKNOWLEDGE) => self.change(Phase::WaitingForStop),
684 (Phase::WaitingForStop, flag::COMPLETE) => self.pause_complete(),
685 (_, 0) => {}
686 _ => self.fail(Failure::unexpected(self.phase as u32, code)),
687 }
688 }
689
690 /// Pop the sector the interrupt announced; store it when a request wants
691 /// it. Fails when the data FIFO never filled.
692 fn take_sector(&mut self) -> Result<(), Failure> {
693 let reading = matches!(self.phase, Phase::Reading | Phase::StartingRead);
694 let (destination, words) = match self.active {
695 Some(a) if reading && !a.cancelled && a.received < a.request.sectors => {
696 // SAFETY: `received < sectors`, so this stays inside the
697 // `sectors * 2048` bytes `Request::new_raw` vouched for.
698 let at = unsafe {
699 a.request
700 .destination
701 .add(a.received as usize * SECTOR_WORDS as usize)
702 };
703 (at, a.request.store_words(a.received))
704 }
705 _ => (core::ptr::null_mut(), 0),
706 };
707 // SAFETY: `destination` is the sector's slot inside the active
708 // request's buffer, valid for `words` words by the contract of
709 // `Request::new_raw` (and unused when `words` is 0).
710 if !unsafe { self.hw.pop_sector(destination, words) } {
711 return Err(Failure::data_not_ready(self.phase as u32));
712 }
713 if words != 0 {
714 if let Some(a) = self.active.as_mut() {
715 a.received += 1;
716 }
717 self.stats.sectors = self.stats.sectors.wrapping_add(1);
718 } else {
719 self.stats.discarded_sectors = self.stats.discarded_sectors.wrapping_add(1);
720 }
721 self.stamp = self.hw.vblank_count();
722 Ok(())
723 }
724
725 /// After a sector was popped and acknowledged: finish the request, chain
726 /// onto the next, or pause for a cancel.
727 fn after_sector(&mut self) {
728 if !matches!(self.phase, Phase::Reading | Phase::StartingRead) {
729 return;
730 }
731 let Some(active) = self.active else {
732 return;
733 };
734 if active.cancelled {
735 self.pause();
736 return;
737 }
738 if active.received < active.request.sectors {
739 return;
740 }
741 self.active = None;
742 self.finish(active, Outcome::Done);
743 match self.take_chain(active.request.end_lba()) {
744 Some(next) => {
745 self.start_chained(next);
746 self.stats.chained = self.stats.chained.wrapping_add(1);
747 }
748 None => self.pause(),
749 }
750 }
751
752 fn pause_complete(&mut self) {
753 if self.recovering && self.active.is_some_and(|a| !a.cancelled) {
754 // The recovery Pause is done: start the seek it was clearing the
755 // way for.
756 self.recovering = false;
757 let location = self.location;
758 self.hw.drop_data_request();
759 self.command(CMD_SETLOC, &location, Phase::SettingTarget);
760 return;
761 }
762 self.recovering = false;
763 self.change(Phase::Done);
764 self.hw.silence_output();
765 if let Some(active) = self.active.take() {
766 self.finish(active, Outcome::Cancelled);
767 }
768 self.settle();
769 }
770
771 // -------------------------------------------------------------- stepping
772
773 fn change(&mut self, phase: Phase) {
774 self.phase = phase;
775 self.stamp = self.hw.vblank_count();
776 }
777
778 /// Send a command; its acknowledge and completion arrive as interrupts.
779 fn command(&mut self, command: u8, params: &[u8], next: Phase) {
780 self.trace(6, command);
781 if !self.hw.issue(command, params) {
782 self.fail(Failure::command_refused(command));
783 return;
784 }
785 self.change(next);
786 }
787
788 fn pause(&mut self) {
789 self.command(CMD_PAUSE, &[], Phase::Stopping);
790 }
791
792 /// End the transfer in the failed state. The active request resumes from
793 /// where it stopped if it has resumes left, else it ends `Failed`.
794 fn fail(&mut self, failure: Failure) {
795 self.trace(4, (failure.0 >> 24) as u8);
796 self.trace(5, (failure.0 >> 8) as u8);
797 self.error = failure.0;
798 self.recovering = false;
799 self.change(Phase::Failed);
800 self.hw.silence_output();
801 self.hw.acknowledge(0);
802 if let Some(active) = self.active.take() {
803 if active.cancelled {
804 self.finish(active, Outcome::Cancelled);
805 } else if active.resumes_left > 0 && active.received < active.request.sectors {
806 self.requeue_remainder(active);
807 } else {
808 self.finish(active, Outcome::Failed(failure));
809 }
810 }
811 self.settle();
812 }
813
814 fn requeue_remainder(&mut self, active: Active) {
815 // SAFETY: `received` is what this transfer stored, so the tail
816 // begins inside the request's destination (`Request::new_raw`).
817 let rest = unsafe { active.request.remaining_after(active.received) };
818 if let Some(request) = rest {
819 self.stats.resumed = self.stats.resumed.wrapping_add(1);
820 self.enqueue(Queued {
821 ticket: active.ticket,
822 request,
823 base: active.base + active.received,
824 seq: 0,
825 resumes_left: active.resumes_left - 1,
826 });
827 }
828 }
829
830 /// The drive has stopped (or is in an unknown state after a failure) and
831 /// no request holds it: grant a waiting lease, start the next request, or
832 /// stay idle.
833 fn settle(&mut self) {
834 if self.owner != Owner::Data || !self.is_ready_to_start() {
835 return;
836 }
837 if self.lease_pending {
838 self.grant_lease();
839 return;
840 }
841 if let Some(index) = self.select(None) {
842 if let Some(queued) = self.queue[index].take() {
843 self.begin(queued);
844 }
845 }
846 }
847
848 fn is_ready_to_start(&self) -> bool {
849 self.active.is_none() && matches!(self.phase, Phase::Idle | Phase::Done | Phase::Failed)
850 }
851
852 /// Start a transfer from a stopped drive.
853 fn begin(&mut self, queued: Queued) {
854 let recover = self.phase == Phase::Failed || self.recover_next;
855 self.recover_next = false;
856 self.error = 0;
857 self.active = Some(Active {
858 ticket: queued.ticket,
859 request: queued.request,
860 base: queued.base,
861 received: 0,
862 cancelled: false,
863 resumes_left: queued.resumes_left,
864 });
865 self.location =
866 psx_io::cd::lba_to_bcd_msf(psx_io::disc_base::shift_lba(queued.request.lba));
867 self.trace(3, 0);
868 if recover {
869 // The drive may still be reading or playing: stop it, then seek.
870 self.recovering = true;
871 self.pause();
872 } else {
873 self.recovering = false;
874 self.hw.drop_data_request();
875 let location = self.location;
876 self.command(CMD_SETLOC, &location, Phase::SettingTarget);
877 }
878 }
879
880 /// Continue reading into the next request with no command in between.
881 fn start_chained(&mut self, queued: Queued) {
882 self.active = Some(Active {
883 ticket: queued.ticket,
884 request: queued.request,
885 base: queued.base,
886 received: 0,
887 cancelled: false,
888 resumes_left: queued.resumes_left,
889 });
890 self.stamp = self.hw.vblank_count();
891 }
892
893 /// The queued request to chain onto a request that ended just before
894 /// `end_lba`: the best-ranked one, if it starts exactly there.
895 fn take_chain(&mut self, end_lba: u32) -> Option<Queued> {
896 // No owner or lease check: the handler only runs for the data owner,
897 // and a lease request aborts the active request before it can end.
898 let index = self.select(Some(end_lba))?;
899 if self.queue[index]?.request.lba != end_lba {
900 return None;
901 }
902 self.queue[index].take()
903 }
904
905 /// Index of the best queued request: most urgent priority first, then
906 /// one that continues from `contiguous_with`, then submission order.
907 fn select(&self, contiguous_with: Option<u32>) -> Option<usize> {
908 let rank = |q: &Queued| -> (Priority, bool, u32) {
909 (
910 q.request.priority,
911 contiguous_with != Some(q.request.lba),
912 q.seq,
913 )
914 };
915 let mut best: Option<usize> = None;
916 for (index, slot) in self.queue.iter().enumerate() {
917 let Some(candidate) = slot else {
918 continue;
919 };
920 let better = match best.and_then(|b| self.queue[b].as_ref()) {
921 None => true,
922 Some(current) => rank(candidate) < rank(current),
923 };
924 if better {
925 best = Some(index);
926 }
927 }
928 best
929 }
930
931 fn enqueue(&mut self, queued: Queued) {
932 if let Some(slot) = self.queue.iter_mut().find(|slot| slot.is_none()) {
933 *slot = Some(queued);
934 }
935 }
936
937 fn new_ticket(&mut self) -> Ticket {
938 let ticket = Ticket(self.next_ticket);
939 self.next_ticket = self.next_ticket.wrapping_add(1).max(1);
940 ticket
941 }
942
943 fn finish(&mut self, active: Active, outcome: Outcome) {
944 match outcome {
945 Outcome::Done => self.stats.requests_done = self.stats.requests_done.wrapping_add(1),
946 Outcome::Cancelled => {
947 self.stats.requests_cancelled = self.stats.requests_cancelled.wrapping_add(1)
948 }
949 Outcome::Failed(_) => {
950 self.stats.requests_failed = self.stats.requests_failed.wrapping_add(1)
951 }
952 }
953 self.record(Completion {
954 ticket: active.ticket,
955 outcome,
956 received: active.base + active.received,
957 });
958 }
959
960 fn record(&mut self, completion: Completion) {
961 let slot = self.results_written as usize % RECENT_RESULTS;
962 self.results[slot] = Some(completion);
963 self.results_written = self.results_written.wrapping_add(1);
964 }
965
966 // ------------------------------------------------------------ the source
967
968 /// Whether the CD interrupt source should reach the CPU right now.
969 pub fn source_wanted(&self) -> bool {
970 match self.owner {
971 Owner::Boot | Owner::Audio => false,
972 Owner::Data => match self.config.irq_mask_policy {
973 IrqMaskPolicy::AlwaysOn => true,
974 IrqMaskPolicy::OnlyWhileBusy => {
975 !self.is_idle() || self.queue.iter().any(Option::is_some)
976 }
977 },
978 }
979 }
980
981 /// Close the CPU-side CD source. The console wrapper calls this before it
982 /// touches the engine from the foreground, so the handler cannot run
983 /// meanwhile; every foreground method ends by opening it again if
984 /// [`source_wanted`](Self::source_wanted).
985 pub fn close_source(&mut self) {
986 self.hw.set_source_enabled(false);
987 self.source_open = false;
988 }
989
990 /// Record that the source was closed behind the engine's back (by the
991 /// console wrapper, through `I_MASK` directly), so the next
992 /// [`sync_source`](Self::sync_source) reopens it.
993 pub fn note_source_closed(&mut self) {
994 self.source_open = false;
995 }
996
997 /// Open or close the source to match [`source_wanted`](Self::source_wanted).
998 pub fn sync_source(&mut self) {
999 let wanted = self.source_wanted();
1000 if wanted != self.source_open {
1001 self.hw.set_source_enabled(wanted);
1002 self.source_open = wanted;
1003 }
1004 }
1005
1006 #[inline(always)]
1007 fn trace(&mut self, kind: u32, value: u8) {
1008 if H::TRACE {
1009 let received = self.active.map_or(0, |a| a.received);
1010 self.hw.trace(
1011 kind << 24 | (self.phase as u32) << 16 | u32::from(value) << 8 | received & 0xff,
1012 );
1013 }
1014 }
1015}