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

psx_cdstream/
lib.rs

1//! Interrupt-driven CD-ROM sector streaming.
2//!
3//! The SDK's `SectorReader` reads a disc by waiting: fine at boot, wrong in
4//! the middle of a game, where the drive delivers a sector every 6.5 ms and a
5//! frame loop that looks once per frame loses most of them. This crate reads
6//! the way a streamer has to: you queue [`Request`]s, the CD interrupt
7//! handler pops one sector per interrupt straight into the request's
8//! destination, and the foreground only submits, cancels and asks how far a
9//! request has got. Nothing busy-waits for the drive.
10//!
11//! # What it does
12//!
13//! - **PIO, one sector per interrupt.** The sector is popped from the data
14//!   FIFO by the CPU. Chopped CD DMA (channel 3) is unreliable on a console;
15//!   the PIO path is the one that has loaded byte-perfect streams there. The
16//!   price is CPU time while a read runs (about 1.3 ms per sector more than a
17//!   working DMA is the figure on record), so read in bursts when there is
18//!   work, not continuously.
19//! - **Seek first.** Every transfer is Setloc, SeekL, Setmode (double speed),
20//!   ReadN: the bracket the BIOS uses. A bare Setloc plus ReadN starts
21//!   delivering while the mechanism is still settling and corrupts a long
22//!   stream on a console.
23//! - **Abort at the next sector.** [`Engine::cancel`] only sets a flag; the
24//!   handler drops the next sector and pauses the drive itself. (A Pause sent
25//!   from the foreground while sectors were arriving lost its acknowledge on
26//!   silicon.) The request ends [`Outcome::Cancelled`] with the sectors that
27//!   had landed; [`Request::remaining_after`] builds the request that
28//!   continues it.
29//! - **A small priority queue.** [`QUEUE_DEPTH`] requests wait behind the
30//!   active one, most urgent [`Priority`] first.
31//! - **Contiguous chaining.** A queued request that starts at the sector
32//!   after the active one's last continues without a Pause or a seek: the
33//!   handler swaps the destination and keeps popping. A layout-ordered group
34//!   of regions costs one seek.
35//! - **Resume.** A request can carry a resume budget
36//!   ([`Request::with_resumes`]): after a drive error the transport pauses,
37//!   seeks back to `lba + received` and carries on.
38//! - **A drive arbiter.** CD-DA and XA playback and data reads cannot share
39//!   the laser. [`Engine::request_audio_lease`] stops the read in flight,
40//!   waits for the drive to stop, closes the CD interrupt source and (on the
41//!   console) hands the controller token to the audio code;
42//!   releasing the lease takes it back and queued reads carry on. End audio
43//!   with Pause, never Stop: after Stop the motor spins down, reports status
44//!   0 for a second or two, and reads started then failed on a console.
45//!   A lease that is asked for and left pending is granted the moment the
46//!   drive stops, ahead of any read queued meanwhile. Audio that must not
47//!   hold up reads uses [`Engine::try_audio_lease`], which takes the drive
48//!   only if it is free and leaves nothing pending, or ends the pending
49//!   request with [`Engine::withdraw_audio_lease`].
50//!
51//! # Use
52//!
53//! ```ignore
54//! // After the polled boot reads are done (SectorReader::release gives the
55//! // token back):
56//! psx_cdstream::install(cd, psx_cdstream::Config::DEFAULT).ok();
57//!
58//! // SAFETY: ROOM is not touched until the request has finished.
59//! let request = unsafe { Request::new_raw(lba, sectors, ROOM.as_mut_ptr().cast()) };
60//! let ticket = psx_cdstream::submit(request.with_priority(Priority::URGENT))?;
61//!
62//! // Each frame:
63//! if let RequestState::Finished(done) = psx_cdstream::state(ticket) {
64//!     // done.outcome, done.received
65//! }
66//! ```
67//!
68//! # How it is built
69//!
70//! [`Engine`] is the whole state machine, generic over [`CdHw`], the
71//! controller as it sees it. On the console [`install`] puts an exception
72//! wrapper in front of psx-rt's handler that calls the one global engine
73//! once per CD interrupt, and the free functions (`submit`, `state`, ...)
74//! reach it with the CD source closed. On the host the tests drive the same
75//! engine against a scripted drive, so every phase, cancel point, error path,
76//! chaining rule and lease transition is exercised without hardware. The
77//! counters are published as `PSX_CD_STATS` ([`StreamStats`]) for tools that
78//! read guest memory.
79//!
80//! Boot-time and blocking loads still use `psx_io::cd::reader::SectorReader`
81//! (or `psx_pack::cd`): it needs the controller token, and so does
82//! `install`, so the compiler keeps the two from sharing the drive.
83//! `uninstall` hands the token back.
84#![no_std]
85#![cfg_attr(target_arch = "mips", feature(asm_experimental_arch))]
86#![deny(unsafe_op_in_unsafe_fn)]
87#![warn(missing_docs)]
88
89mod engine;
90mod hw;
91mod request;
92
93#[cfg(any(target_arch = "mips", doc))]
94mod console;
95#[cfg(any(target_arch = "mips", doc))]
96pub use console::*;
97
98pub use engine::*;
99pub use hw::CdHw;
100pub use request::*;
101
102#[cfg(test)]
103extern crate std;
104#[cfg(test)]
105mod fake;
106#[cfg(test)]
107mod tests;