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

psx_fmv/
player.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! A streaming STR player a game can call: [`play`] streams one movie from
3//! the disc at double speed, decodes it through the MDEC and shows it, with
4//! its interleaved XA audio going straight from the drive to the SPU.
5//!
6//! The pipeline is the `hello-fmv` console test's, without the test's
7//! sector stamps and overlay:
8//!
9//! 1. A polled pump drains every sector the drive has ready into a ring of
10//!    three frame slots. It runs between every unit of work, including
11//!    every wait and every two macroblocks of the bitstream decode.
12//! 2. [`crate::bitstream::decode_frame`] turns the next frame's bitstream into one
13//!    of two run-length buffers while the MDEC decodes the previous frame
14//!    from the other.
15//! 3. Decoded 16-pixel columns come back over DMA1 into one of two column
16//!    buffers and go to a free display buffer over DMA2 while DMA1 fills
17//!    the other.
18//! 4. The flip is queued to the VBlank handler. There are three display
19//!    buffers, so the MDEC can start the next frame while the last one
20//!    waits for its flip.
21//!
22//! Pacing follows the drive, not the VBlank: a frame is shown
23//! [`Config::latency_vblanks`] after its last sector arrived. The audio
24//! plays as its sectors arrive, so picture and sound keep a fixed offset
25//! for the whole movie whatever the display's refresh rate, and a 25 fps
26//! PAL movie plays at 25 fps on a 60 Hz display. A frame completed while an
27//! older one still waits for the decoder replaces it (counted in
28//! [`Outcome::skipped`]).
29//!
30//! Memory comes from the caller ([`memory_words`] says how much), so a
31//! game lends a buffer that is idle while the movie plays, such as a level
32//! buffer at boot. The player takes over the CD drive, the MDEC, DMA
33//! channels 0 to 2, root counter 2, the display and the VRAM under
34//! [`Config::buffers`] while it runs. No frame of the caller's may be in
35//! flight on the GPU when it starts, and the SPU must already be
36//! initialised.
37
38use core::ptr::addr_of_mut;
39
40use psx_gpu::prim::FillRect;
41use psx_gpu::Gpu;
42use psx_hw::gpu::gp1;
43use psx_hw::mdec::{DECODE_15BPP, DECODE_24BPP};
44use psx_io::periph::{Cd, GpuDma, MdecDma};
45use psx_pack::cd::{SectorReader, SECTOR_WORDS};
46use psx_rt::{interrupts, Peripherals};
47use psx_spu::CdVolume;
48use psx_vram::VramRect;
49
50use crate::mdec::Mdec;
51use crate::stream::{Chunk, FrameAssembler, CHUNK_PAYLOAD_BYTES, MAX_CHUNKS};
52use crate::{bitstream, mdec};
53
54/// One movie on the disc, as the build measured it.
55#[derive(Copy, Clone, Debug)]
56pub struct Movie {
57    /// First sector of the file.
58    pub lba: u32,
59    /// Frames in the movie; the stream ends after the last.
60    pub frames: u32,
61    /// Picture width, a multiple of 16.
62    pub width: u16,
63    /// Picture height, a multiple of 16.
64    pub height: u16,
65    /// Most sectors any frame spans.
66    pub max_chunks: u16,
67    /// Most run-length words any frame decodes to
68    /// ([`crate::bitstream::decode_frame`]'s result).
69    pub rle_words: usize,
70    /// XA audio file and channel to play, or `None` for a silent movie.
71    pub xa: Option<(u8, u8)>,
72}
73
74/// Display buffers at (0, 0), (0, 256) and (512, 0): room for 320 pixels
75/// at 24 bits (480 halfwords) by up to 256 lines each.
76pub const DEFAULT_BUFFERS: [(u16, u16); 3] = [(0, 0), (0, 256), (512, 0)];
77
78/// Colour depth of the decoded picture.
79#[derive(Copy, Clone, Debug, PartialEq, Eq)]
80pub enum Depth {
81    /// 15-bit: the GPU can still draw over the movie (subtitles, a skip
82    /// prompt), at the price of 5-bit banding on smooth gradients.
83    Rgb15,
84    /// 24-bit, as most commercial players show their movies: no banding,
85    /// 1.5 times the MDEC output and VRAM upload, and the display runs in
86    /// 24-bit mode, where the GPU cannot draw. The player switches the
87    /// display mode for the movie and restores it on return.
88    Rgb24,
89}
90
91/// Where and how to show it.
92#[derive(Copy, Clone, Debug)]
93pub struct Config {
94    /// Output depth.
95    pub depth: Depth,
96    /// VRAM origins (halfword x, line y) of the three display buffers, each
97    /// `screen_width` pixels by `screen_height` lines; the movie is centred
98    /// in them over black. [`DEFAULT_BUFFERS`] suits a 320-wide display.
99    pub buffers: [(u16, u16); 3],
100    /// The display's size.
101    pub screen_width: u16,
102    /// Display height.
103    pub screen_height: u16,
104    /// VBlanks from a frame's last sector to its flip: the decoder's
105    /// headroom, and the fixed lag of the picture behind the sound.
106    pub latency_vblanks: u32,
107    /// End the stream if no sector arrives for this many VBlanks.
108    pub stall_vblanks: u32,
109}
110
111/// Why [`play`] returned.
112#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
113pub enum Stop {
114    /// The last frame was shown.
115    #[default]
116    End,
117    /// The caller's poll asked to stop.
118    Skipped,
119    /// No sector for [`Config::stall_vblanks`].
120    Stall,
121    /// The drive reported an error.
122    CdError,
123    /// The MDEC failed [`WEDGE_ERRORS`] frames in a row.
124    Wedged,
125    /// Setup failed (drive, MDEC or memory); nothing played.
126    Setup,
127}
128
129/// What happened, for logs and tests.
130#[derive(Copy, Clone, Debug, Default)]
131pub struct Outcome {
132    /// Why it ended.
133    pub stop: Stop,
134    /// Frames shown.
135    pub shown: u32,
136    /// Frames replaced before the decoder reached them.
137    pub skipped: u32,
138    /// Frames shown later than their due VBlank, and the worst lateness.
139    pub late: u32,
140    /// Most VBlanks any frame was shown after it was due.
141    pub worst_late_vblanks: u32,
142    /// Frames abandoned with sectors missing, and bitstream or MDEC
143    /// failures.
144    pub dropped: u32,
145    /// Frames the bitstream decoder or the MDEC failed.
146    pub decode_errors: u32,
147    /// Number of the last complete frame the stream delivered.
148    pub last_frame: u32,
149    /// VBlanks from the start of the stream to the return.
150    pub vblanks: u32,
151    /// CPU per decoded frame, in kilocycles: bitstream decode, MDEC and
152    /// upload, and waiting.
153    pub kcyc_vlc: u32,
154    /// MDEC and upload time not overlapped with the bitstream decode.
155    pub kcyc_mdec: u32,
156    /// Waiting for sectors or the flip.
157    pub kcyc_wait: u32,
158}
159
160/// Consecutive MDEC failures that end a movie as [`Stop::Wedged`].
161pub const WEDGE_ERRORS: u32 = 8;
162/// Frame slots: one filling, one ready, one being decoded.
163const SLOTS: usize = 3;
164/// Words of bitstream one sector carries.
165const CHUNK_WORDS: usize = CHUNK_PAYLOAD_BYTES / 4;
166/// Longest a frame may take from its decode command to its last column in
167/// VRAM before it counts as a decode error.
168const DECODE_TIMEOUT_VBLANKS: u32 = 30;
169/// Macroblocks between pumps inside the bitstream decode.
170const PUMP_MACROBLOCKS: u32 = 2;
171/// Double speed, XA-ADPCM to the SPU, file/channel filter.
172const MODE_XA: u8 = 0x80 | 0x40 | 0x08;
173/// Double speed, data only.
174const MODE_DATA: u8 = 0x80;
175/// Non-video sectors in a row that end the stream: past the file's end,
176/// when the last frame never completed.
177const FOREIGN_SECTORS: u32 = 32;
178/// GP1(08h), display mode, and its 24-bit colour bit.
179const GP1_DISPLAY_MODE: u32 = 0x0800_0000;
180const DISPLAY_24BIT: u32 = 1 << 4;
181
182/// The current GP1(08h) parameter, rebuilt from GPUSTAT bits 16..22.
183fn display_mode() -> u32 {
184    let stat = psx_io::gpu::status().bits();
185    ((stat >> 17) & 3)
186        | ((stat >> 19) & 1) << 2
187        | ((stat >> 20) & 1) << 3
188        | ((stat >> 21) & 1) << 4
189        | ((stat >> 22) & 1) << 5
190        | ((stat >> 16) & 1) << 6
191        | ((stat >> 14) & 1) << 7
192}
193
194/// Display buffers: one shown, one decoded and waiting for its flip, one
195/// the MDEC decodes into. With two, the MDEC would sit idle from a frame's
196/// end until that frame's flip, which a 24-bit movie cannot afford.
197const BUFFERS: usize = 3;
198
199fn slot_words(m: &Movie) -> usize {
200    m.max_chunks as usize * CHUNK_WORDS
201}
202
203/// Run-length buffer words: the measured maximum, rounded up to whole DMA
204/// blocks.
205fn rle_words(m: &Movie) -> usize {
206    m.rle_words.div_ceil(mdec::DMA_BLOCK_WORDS) * mdec::DMA_BLOCK_WORDS
207}
208
209/// VRAM halfwords one decoded pixel takes.
210fn halfwords_x2(depth: Depth) -> u16 {
211    match depth {
212        Depth::Rgb15 => 2,
213        Depth::Rgb24 => 3,
214    }
215}
216
217/// One column, 16 pixels wide: 16 * height pixels of 2 or 3 bytes.
218fn column_words(m: &Movie, depth: Depth) -> usize {
219    4 * halfwords_x2(depth) as usize * m.height as usize
220}
221
222/// Words of memory [`play`] needs for `movie` at `depth`.
223pub fn memory_words(movie: &Movie, depth: Depth) -> usize {
224    SLOTS * slot_words(movie) + 2 * rle_words(movie) + 2 * column_words(movie, depth)
225}
226
227/// Root counter 2 (system clock / 8) accounting of where the time goes.
228const PHASE_VLC: usize = 0;
229const PHASE_MDEC: usize = 1;
230const PHASE_WAIT: usize = 2;
231
232struct Clock {
233    phase: usize,
234    last: u16,
235    acc: [u32; 3],
236}
237
238impl Clock {
239    fn switch(&mut self, next: usize) {
240        let now = psx_io::timers::counter(psx_io::timers::Timer::Timer2);
241        self.acc[self.phase] += now.wrapping_sub(self.last) as u32;
242        self.last = now;
243        self.phase = next;
244    }
245}
246
247/// Raw views of the caller's memory. The pieces are disjoint; the player
248/// hands each to one user at a time (see `Stream` and `Player`).
249#[derive(Copy, Clone)]
250struct Buffers {
251    slots: *mut u32,
252    slot_words: usize,
253    rle: *mut u32,
254    rle_words: usize,
255    columns: *mut u32,
256    column_words: usize,
257}
258
259impl Buffers {
260    fn slot(&self, i: usize) -> &'static mut [u8] {
261        // SAFETY: slot `i` lies inside the caller's buffer (memory_words),
262        // and the stream fills only the slot the decoder is not reading.
263        unsafe {
264            core::slice::from_raw_parts_mut(
265                self.slots.add(i * self.slot_words) as *mut u8,
266                self.slot_words * 4,
267            )
268        }
269    }
270    fn rle(&self, i: usize) -> &'static mut [u32] {
271        // SAFETY: as above; the bitstream decoder writes one buffer while
272        // DMA0 reads the other.
273        unsafe { core::slice::from_raw_parts_mut(self.rle.add(i * self.rle_words), self.rle_words) }
274    }
275    fn rle16(&self, i: usize) -> &'static mut [u16] {
276        let words = self.rle(i);
277        // SAFETY: the same buffer as halfwords.
278        unsafe { core::slice::from_raw_parts_mut(words.as_mut_ptr() as *mut u16, words.len() * 2) }
279    }
280    fn column(&self, i: usize) -> *mut u32 {
281        // SAFETY: column `i` lies inside the caller's buffer.
282        unsafe { self.columns.add(i * self.column_words) }
283    }
284}
285
286static mut READER: Option<SectorReader> = None;
287static mut MDEC: Option<Mdec> = None;
288static mut SECTOR: [u32; SECTOR_WORDS] = [0; SECTOR_WORDS];
289
290/// Own the drive and the MDEC from here on, replacing any earlier owner.
291fn install_devices(cd: Cd, mdec: MdecDma) {
292    // SAFETY: single-threaded; nothing holds a reference from before.
293    unsafe {
294        *addr_of_mut!(READER) = Some(SectorReader::with_cd(cd));
295        *addr_of_mut!(MDEC) = Some(Mdec::new(mdec));
296    }
297}
298
299fn reader() -> &'static mut SectorReader {
300    // SAFETY: single-threaded; only the player uses its reader, and no
301    // caller keeps the reference across a call that takes another.
302    unsafe { (*addr_of_mut!(READER)).as_mut().expect("reader installed") }
303}
304
305fn mdec() -> &'static mut Mdec {
306    // SAFETY: as `reader`.
307    unsafe { (*addr_of_mut!(MDEC)).as_mut().expect("mdec installed") }
308}
309
310/// Read one sector synchronously into the player's sector buffer.
311fn read_one(lba: u32) -> Option<&'static [u8]> {
312    let r = reader();
313    // SAFETY: single-threaded use of the reader and its sector buffer.
314    unsafe {
315        if !r.start_read(lba) {
316            return None;
317        }
318        let ok = r.read_sector(&mut *addr_of_mut!(SECTOR));
319        r.stop();
320        if !ok {
321            return None;
322        }
323        Some(core::slice::from_raw_parts(
324            addr_of_mut!(SECTOR) as *const u8,
325            SECTOR_WORDS * 4,
326        ))
327    }
328}
329
330/// Find a file in the disc's root directory by name: `(lba, bytes)`.
331/// Takes the drive over (data mode, double speed) and leaves it stopped.
332pub fn find_root_file(name: &str) -> Option<(u32, u32)> {
333    // SAFETY: the caller is not using the drive or the MDEC, as the module
334    // docs say; the player takes them over for as long as it runs.
335    let devices = unsafe { Peripherals::steal() };
336    install_devices(devices.cd, devices.mdec_dma);
337    if !reader().prepare() {
338        return None;
339    }
340    let (root, _) = crate::iso::root_directory(read_one(crate::iso::PVD_LBA)?)?;
341    crate::iso::find_in_directory(read_one(root)?, name)
342}
343
344/// The drive side: sectors into frame slots.
345struct Stream {
346    buf: Buffers,
347    last_frame: u32,
348    frames: u32,
349    done: bool,
350    cd_error: bool,
351    started: bool,
352    /// Sectors in a row that were not video.
353    foreign: u32,
354    filling: usize,
355    /// A complete frame waiting for the decoder: (slot, bytes, VBlank its
356    /// last sector arrived).
357    ready: Option<(usize, u32, u32)>,
358    decoding: Option<usize>,
359    asm: FrameAssembler,
360    skipped: u32,
361    last_sector_vblank: u32,
362}
363
364impl Stream {
365    fn pump(&mut self) {
366        while !self.done {
367            // SAFETY: single-threaded use of the player's reader and sector.
368            let got = unsafe { reader().try_read_sector(&mut *addr_of_mut!(SECTOR)) };
369            match got {
370                Ok(true) => {}
371                Ok(false) => return,
372                Err(()) => {
373                    self.cd_error = true;
374                    self.done = true;
375                    return;
376                }
377            }
378            let now = interrupts::vblank_count();
379            self.last_sector_vblank = now;
380            // SAFETY: SECTOR as bytes.
381            let sector = unsafe {
382                core::slice::from_raw_parts(addr_of_mut!(SECTOR) as *const u8, SECTOR_WORDS * 4)
383            };
384            if Chunk::parse(sector).is_none() {
385                // A movie may carry empty data sectors in its audio slots
386                // (WipEout's last seconds are silent), but a long run of
387                // them means the drive has read past the file's end.
388                self.foreign += 1;
389                if self.started && self.foreign > FOREIGN_SECTORS {
390                    self.done = true;
391                }
392                continue;
393            }
394            self.foreign = 0;
395            self.started = true;
396            if let Some(frame) = self.asm.add(sector, self.buf.slot(self.filling)) {
397                self.last_frame = frame.number;
398                if frame.number >= self.frames {
399                    self.done = true;
400                }
401                let done = self.filling;
402                let next = match self.ready.replace((done, frame.size, now)) {
403                    Some((stale, _, _)) => {
404                        self.skipped += 1;
405                        stale
406                    }
407                    None => (0..SLOTS)
408                        .find(|&s| s != done && Some(s) != self.decoding)
409                        .unwrap_or(done),
410                };
411                self.filling = next;
412            }
413        }
414    }
415}
416
417/// The MDEC's current frame, one column at a time: DMA1 into a column
418/// buffer, then DMA2 into the back buffer while DMA1 fills the other.
419struct Columns {
420    /// Left edge and column width, in VRAM halfwords.
421    x: u16,
422    step: u16,
423    y: u16,
424    count: u16,
425    next: u16,
426    reading: Option<(u16, usize)>,
427    filled: Option<(u16, usize)>,
428    uploading: Option<usize>,
429    started: u32,
430    /// DMA1 refused a column: the frame cannot complete.
431    broken: bool,
432}
433
434impl Columns {
435    fn start(x: u16, step: u16, y: u16, count: u16) -> Self {
436        Columns {
437            x,
438            step,
439            y,
440            count,
441            next: 0,
442            reading: None,
443            filled: None,
444            uploading: None,
445            started: interrupts::vblank_count(),
446            broken: false,
447        }
448    }
449
450    /// Move the transfers along; `true` once every column is in VRAM.
451    fn service(&mut self, dma: &mut GpuDma, buf: &Buffers, height: u16) -> bool {
452        if let Some(done) = self.reading {
453            if mdec().column_done() {
454                self.reading = None;
455                self.filled = Some(done);
456            }
457        }
458        if self.uploading.is_some() && psx_vram::dma_copy_to_vram_done() {
459            self.uploading = None;
460        }
461        if self.uploading.is_none() {
462            if let Some((c, b)) = self.filled.take() {
463                let rect = VramRect::new(self.x + c * self.step, self.y, self.step, height);
464                // SAFETY: column buffer `b` holds the rectangle's words, and
465                // DMA1 does not refill it until this upload is done. Nothing
466                // else uses GP0 or channel 2 while a frame decodes: the
467                // player only draws between frames, after `uploading` is
468                // clear.
469                if unsafe { psx_vram::dma_copy_to_vram_start(dma, rect, buf.column(b)) } {
470                    self.uploading = Some(b);
471                } else {
472                    // SAFETY: the column buffer holds `column_words` words.
473                    let words =
474                        unsafe { core::slice::from_raw_parts(buf.column(b), buf.column_words) };
475                    psx_vram::upload_words(rect, words);
476                }
477            }
478        }
479        if self.reading.is_none() && self.next < self.count {
480            let held = |b: usize| self.filled.map(|f| f.1) == Some(b) || self.uploading == Some(b);
481            if let Some(b) = (0..2).find(|&b| !held(b)) {
482                // SAFETY: buffer `b` is free (see `held`) and holds
483                // `column_words` words.
484                if unsafe { mdec().read_column_start(buf.column(b), buf.column_words) } {
485                    self.reading = Some((self.next, b));
486                    self.next += 1;
487                } else {
488                    // A column the MDEC channel cannot express: abandon the
489                    // frame as a decode error rather than spin on it.
490                    self.next = self.count;
491                    self.broken = true;
492                }
493            }
494        }
495        self.next == self.count
496            && self.reading.is_none()
497            && self.filled.is_none()
498            && self.uploading.is_none()
499    }
500}
501
502struct Player {
503    /// The GPU's channel 2: the column uploads and the flip's GP0(1Fh) go
504    /// through it.
505    dma: GpuDma,
506    st: Stream,
507    buf: Buffers,
508    movie: Movie,
509    /// Movie's left edge and column width in VRAM halfwords.
510    x: u16,
511    step: u16,
512    y_offset: u16,
513    latency: u32,
514    /// The three display buffers' VRAM origins.
515    origins: [(u16, u16); BUFFERS],
516    /// The buffer on screen.
517    front: usize,
518    /// The MDEC's frame: (RLE buffer, display buffer, due VBlank).
519    decoding: Option<(usize, usize, u32)>,
520    cols: Columns,
521    /// Decoded frames waiting for their flip, oldest first: (display
522    /// buffer, due VBlank).
523    waiting: [Option<(usize, u32)>; 2],
524    /// The buffer whose flip is queued, until the VBlank handler applies it.
525    queued: Option<usize>,
526    /// A bitstream-decoded frame waiting for the MDEC: (RLE buffer, words,
527    /// due VBlank).
528    pending: Option<(usize, usize, u32)>,
529    shown: u32,
530    late: u32,
531    worst_late: u32,
532    errors: u32,
533    in_a_row: u32,
534    wedged: bool,
535    clock: Clock,
536}
537
538impl Player {
539    fn idle_phase(&self) -> usize {
540        if self.decoding.is_some() {
541            PHASE_MDEC
542        } else {
543            PHASE_WAIT
544        }
545    }
546
547    /// A display buffer that is neither shown, nor waiting for or queued to
548    /// its flip, nor being decoded into.
549    fn free_buffer(&self) -> Option<usize> {
550        (0..BUFFERS).find(|&b| {
551            b != self.front
552                && self.queued != Some(b)
553                && self.decoding.map(|d| d.1) != Some(b)
554                && self.waiting.iter().all(|w| w.map(|w| w.0) != Some(b))
555        })
556    }
557
558    /// Nothing decoded, decoding or waiting to be shown.
559    fn drained(&self) -> bool {
560        self.decoding.is_none()
561            && self.pending.is_none()
562            && self.waiting[0].is_none()
563            && self.queued.is_none()
564    }
565
566    /// Drain the drive, move the MDEC's transfers along, and queue or
567    /// retire flips.
568    fn tick(&mut self) {
569        // Sample the clock at least once per pump: root counter 2 wraps
570        // after 65,536 * 8 cycles, less than one bitstream decode.
571        self.clock.switch(self.clock.phase);
572        self.st.pump();
573        if let Some(b) = self.queued {
574            if !interrupts::is_display_control_queued() {
575                self.front = b;
576                self.queued = None;
577                self.shown += 1;
578            }
579        }
580        if let Some((_, b, due)) = self.decoding {
581            if self
582                .cols
583                .service(&mut self.dma, &self.buf, self.movie.height)
584                && !self.cols.broken
585            {
586                self.decoding = None;
587                if mdec().decode_finish() {
588                    self.in_a_row = 0;
589                    let slot = if self.waiting[0].is_none() { 0 } else { 1 };
590                    self.waiting[slot] = Some((b, due));
591                } else {
592                    self.decode_failed();
593                }
594            } else if self.cols.broken
595                || interrupts::vblank_count().wrapping_sub(self.cols.started)
596                    > DECODE_TIMEOUT_VBLANKS
597            {
598                mdec().abort_decode();
599                psx_io::dma::abort(psx_io::dma::Channel::Gpu);
600                // GP1(01h): drop a VRAM copy the abort left waiting for
601                // pixels.
602                psx_io::gpu::write_display_control(gp1::RESET_CMD_BUFFER);
603                self.decoding = None;
604                self.decode_failed();
605            }
606        }
607        if self.queued.is_none() {
608            if let Some((b, due)) = self.waiting[0] {
609                let now = interrupts::vblank_count();
610                let edge = now.wrapping_add(1);
611                // Due at the next edge, or behind: more decoded frames
612                // already wait, so show this one now rather than skip one
613                // later.
614                let behind = self.waiting[1].is_some()
615                    || (self.pending.is_some() && self.st.ready.is_some());
616                // GP0 is free only between column uploads.
617                let gp0_free = self.cols.uploading.is_none() && psx_vram::dma_copy_to_vram_done();
618                if gp0_free && (behind || edge.wrapping_sub(due) as i32 >= 0) {
619                    // The VBlank handler flips only once GPUSTAT bit 24 says
620                    // the GPU got past this frame's uploads: GP0(1Fh) after
621                    // them, acknowledged afresh now that no flip is queued.
622                    let gpu = Gpu::from_dma_mut(&mut self.dma);
623                    gpu.arm_draw_done();
624                    gpu.signal_draw_done();
625                    let (x, y) = self.origins[b];
626                    interrupts::queue_display_control_at_vblank(gp1::display_start(
627                        x as u32, y as u32,
628                    ));
629                    let late = edge.wrapping_sub(due) as i32;
630                    if late > 0 {
631                        self.late += 1;
632                        self.worst_late = self.worst_late.max(late as u32);
633                    }
634                    self.queued = Some(b);
635                    self.waiting = [self.waiting[1], None];
636                }
637            }
638        }
639    }
640
641    fn decode_failed(&mut self) {
642        self.errors += 1;
643        self.in_a_row += 1;
644        if self.in_a_row >= WEDGE_ERRORS {
645            self.wedged = true;
646        }
647        let _ = setup_mdec();
648    }
649}
650
651fn setup_mdec() -> bool {
652    mdec().reset() && mdec().load_tables().is_some_and(|t| t.enable_writes != 0)
653}
654
655/// Every display buffer black. A 24-bit line is 1.5 times as wide in VRAM
656/// halfwords.
657fn clear_buffers(dma: &mut GpuDma, config: &Config) {
658    let gpu = Gpu::from_dma_mut(dma);
659    // A frame the caller kicked before the movie may still be walking.
660    gpu.wait_idle();
661    let line = config.screen_width * halfwords_x2(config.depth) / 2;
662    for &(x, y) in &config.buffers {
663        gpu.draw(&FillRect::new(
664            (x, y),
665            (line, config.screen_height),
666            (0, 0, 0),
667        ));
668    }
669    gpu.wait_idle();
670}
671
672/// Hand the display back in the caller's `mode`, over black when the movie
673/// ran at 24 bits: that picture read as 15-bit pixels would be noise.
674fn leave_display(dma: &mut GpuDma, config: &Config, mode: u32) {
675    if config.depth == Depth::Rgb24 {
676        clear_buffers(dma, config);
677        psx_io::gpu::write_display_control(GP1_DISPLAY_MODE | mode);
678    }
679}
680
681fn setup_failed() -> Outcome {
682    Outcome {
683        stop: Stop::Setup,
684        ..Outcome::default()
685    }
686}
687
688/// Play `movie` until it ends or `poll` returns `true` (checked once per
689/// VBlank, e.g. for a skip button). `memory` must hold at least
690/// [`memory_words`] words and is free again on return. At 15 bits the last
691/// frame stays on screen (the display start may be any of the three
692/// buffers); at 24 bits every buffer is cleared to black and the caller's
693/// display mode is restored. The drive is stopped
694/// and the CD audio volume left at 0.
695pub fn play(
696    movie: &Movie,
697    config: &Config,
698    memory: &mut [u32],
699    poll: &mut dyn FnMut() -> bool,
700) -> Outcome {
701    if memory.len() < memory_words(movie, config.depth)
702        || movie.width == 0
703        || movie.height == 0
704        || !movie.width.is_multiple_of(16)
705        || !movie.height.is_multiple_of(16)
706        || movie.width > config.screen_width
707        || movie.height > config.screen_height
708        || movie.max_chunks == 0
709        || movie.max_chunks > MAX_CHUNKS
710    {
711        return setup_failed();
712    }
713    let base = memory.as_mut_ptr();
714    let sw = slot_words(movie);
715    let rw = rle_words(movie);
716    let buf = Buffers {
717        slots: base,
718        slot_words: sw,
719        // SAFETY: offsets inside `memory` (checked against memory_words).
720        rle: unsafe { base.add(SLOTS * sw) },
721        rle_words: rw,
722        // SAFETY: as above.
723        columns: unsafe { base.add(SLOTS * sw + 2 * rw) },
724        column_words: column_words(movie, config.depth),
725    };
726
727    // SAFETY: the player takes over the drive, the MDEC and the GPU's DMA
728    // channel, as its doc says; nothing else drives them until it returns.
729    let devices = unsafe { Peripherals::steal() };
730    install_devices(devices.cd, devices.mdec_dma);
731    let mut dma = devices.gpu_dma;
732
733    let r = reader();
734    // The player owns the drive until it returns; prepare takes it over from
735    // whatever used it before. Unmute: a muted drive plays no XA.
736    let ready = {
737        match movie.xa {
738            Some((file, channel)) => {
739                r.prepare_mode(MODE_XA) && r.unmute() && r.set_filter(file, channel)
740            }
741            None => r.prepare_mode(MODE_DATA),
742        }
743    };
744    if !ready || !setup_mdec() {
745        return setup_failed();
746    }
747    if movie.xa.is_some() {
748        reader().cd_mut().set_audio_mixer(0x80, 0, 0x80, 0);
749        psx_spu::set_cd_volume(CdVolume::MAX, CdVolume::MAX);
750        psx_spu::enable_cd_audio(true);
751    }
752
753    // Every display buffer black, the first one shown, in 24-bit mode if the
754    // movie is.
755    let x2 = halfwords_x2(config.depth);
756    let _ = interrupts::take_queued_display_control();
757    clear_buffers(&mut dma, config);
758    let (x0, y0) = config.buffers[0];
759    psx_io::gpu::write_display_control(gp1::display_start(x0 as u32, y0 as u32));
760    let mode = display_mode();
761    if config.depth == Depth::Rgb24 {
762        psx_io::gpu::write_display_control(GP1_DISPLAY_MODE | mode | DISPLAY_24BIT);
763    }
764
765    // The reader was prepared above.
766    if !r.start_read(movie.lba) {
767        leave_display(&mut dma, config, mode);
768        return setup_failed();
769    }
770
771    psx_io::timers::set_mode(psx_io::timers::Timer::Timer2, 0x0200);
772    let start = interrupts::vblank_count();
773    let mut p = Player {
774        dma,
775        st: Stream {
776            buf,
777            last_frame: 0,
778            frames: movie.frames,
779            done: false,
780            cd_error: false,
781            started: false,
782            foreign: 0,
783            filling: 0,
784            ready: None,
785            decoding: None,
786            asm: FrameAssembler::new(),
787            skipped: 0,
788            last_sector_vblank: start,
789        },
790        buf,
791        movie: *movie,
792        x: (config.screen_width - movie.width) / 2 * x2 / 2,
793        step: 8 * x2,
794        y_offset: (config.screen_height - movie.height) / 2,
795        latency: config.latency_vblanks,
796        origins: config.buffers,
797        front: 0,
798        decoding: None,
799        cols: Columns::start(0, 0, 0, 0),
800        waiting: [None, None],
801        queued: None,
802        pending: None,
803        shown: 0,
804        late: 0,
805        worst_late: 0,
806        errors: 0,
807        in_a_row: 0,
808        wedged: false,
809        clock: Clock {
810            phase: PHASE_WAIT,
811            last: psx_io::timers::counter(psx_io::timers::Timer::Timer2),
812            acc: [0; 3],
813        },
814    };
815    let mut polled = start;
816    let mut skipped = false;
817    let rows = movie.height as u32 / 16;
818    let macroblocks = movie.width as u32 / 16 * rows;
819    loop {
820        p.tick();
821        if p.wedged {
822            break;
823        }
824        let now = interrupts::vblank_count();
825        if now != polled {
826            polled = now;
827            if poll() {
828                skipped = true;
829                break;
830            }
831        }
832        // Decode the next frame's bitstream while the MDEC works on this one.
833        if p.pending.is_none() {
834            if let Some((slot, bytes, arrived)) = p.st.ready.take() {
835                p.clock.switch(PHASE_VLC);
836                p.st.decoding = Some(slot);
837                let rle = match p.decoding {
838                    Some((r, _, _)) => 1 - r,
839                    None => 0,
840                };
841                let frame_bytes = &p.buf.slot(slot)[..(bytes as usize).min(p.buf.slot_words * 4)];
842                let out = p.buf.rle16(rle);
843                let decoded = bitstream::decode_frame(
844                    frame_bytes,
845                    out,
846                    macroblocks,
847                    PUMP_MACROBLOCKS,
848                    &mut || p.tick(),
849                );
850                p.st.decoding = None;
851                match decoded {
852                    Ok(words) => p.pending = Some((rle, words, arrived.wrapping_add(p.latency))),
853                    Err(_) => p.errors += 1,
854                }
855                let phase = p.idle_phase();
856                p.clock.switch(phase);
857                continue;
858            }
859        }
860        if p.decoding.is_none() && p.pending.is_some() {
861            if let Some(b) = p.free_buffer() {
862                let Some((rle, words, due)) = p.pending.take() else {
863                    continue;
864                };
865                p.clock.switch(PHASE_MDEC);
866                let depth = match config.depth {
867                    Depth::Rgb15 => DECODE_15BPP,
868                    Depth::Rgb24 => DECODE_24BPP,
869                };
870                // SAFETY: the bitstream decoder writes only the other buffer
871                // until this decode is finished or given up.
872                if unsafe { mdec().decode_start(p.buf.rle(rle), words, depth) }.is_err() {
873                    // Nothing the decoder wrote can be sent (no whole block):
874                    // nothing started, the frame is lost.
875                    p.errors += 1;
876                    continue;
877                }
878                let (x, y) = p.origins[b];
879                p.cols = Columns::start(x + p.x, p.step, y + p.y_offset, movie.width / 16);
880                p.decoding = Some((rle, b, due));
881                continue;
882            }
883        }
884        let idle = now.wrapping_sub(p.st.last_sector_vblank);
885        if p.drained() && p.st.ready.is_none() && (p.st.done || idle > config.stall_vblanks) {
886            break;
887        }
888        let phase = p.idle_phase();
889        p.clock.switch(phase);
890    }
891    // A flip still queued would land after we hand the display back.
892    let _ = interrupts::take_queued_display_control();
893    mdec().abort_decode();
894    psx_io::dma::abort(psx_io::dma::Channel::Gpu);
895    if p.cols.uploading.is_some() {
896        // The abort cut a VRAM copy short: drop the transfer the GPU still
897        // waits to receive.
898        psx_io::gpu::write_display_control(gp1::RESET_CMD_BUFFER);
899    }
900    // Stop the stream we started (also ends the XA audio).
901    reader().stop();
902    if movie.xa.is_some() {
903        psx_spu::set_cd_volume(CdVolume(0), CdVolume(0));
904    }
905    if config.depth == Depth::Rgb24 {
906        leave_display(&mut p.dma, config, mode);
907    }
908    let stop = if skipped {
909        Stop::Skipped
910    } else if p.wedged {
911        Stop::Wedged
912    } else if p.st.cd_error {
913        Stop::CdError
914    } else if p.st.done {
915        Stop::End
916    } else {
917        Stop::Stall
918    };
919    let per = (p.shown + p.st.skipped).max(1);
920    let kcyc = |phase: usize| p.clock.acc[phase] / per * 8 / 1000;
921    Outcome {
922        stop,
923        shown: p.shown,
924        skipped: p.st.skipped,
925        late: p.late,
926        worst_late_vblanks: p.worst_late,
927        dropped: p.st.asm.dropped,
928        decode_errors: p.errors,
929        last_frame: p.st.last_frame,
930        vblanks: interrupts::vblank_count().wrapping_sub(start),
931        kcyc_vlc: kcyc(PHASE_VLC),
932        kcyc_mdec: kcyc(PHASE_MDEC),
933        kcyc_wait: kcyc(PHASE_WAIT),
934    }
935}