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

psx_fmv/
mdec.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! MDEC driver: table upload, decode command, DMA0 in, DMA1 out.
3//!
4//! [`Mdec`] owns the two DMA channels that feed and drain the decoder; every
5//! step that starts one is a method on it.
6//!
7//! Decode protocol, as commercial players drive it:
8//!
9//! 1. [`Mdec::reset`] once, then [`Mdec::load_tables`] (quantization + IDCT
10//!    tables). Neither assumes the MDEC reacts at once: see "Reset latency"
11//!    below.
12//! 2. Per frame, [`Mdec::decode_start`] writes the decode command (output depth +
13//!    run-length word count) to MDEC0 and kicks DMA0 with the whole
14//!    run-length buffer. It does not wait: the MDEC throttles DMA0 as its
15//!    output FIFO fills.
16//! 3. [`Mdec::read_column`] pulls one 16-pixel-wide column of decoded
17//!    macroblocks over DMA1 (macroblocks come out in the order the
18//!    bitstream stores them: top to bottom, then left to right), which the
19//!    caller uploads to VRAM.
20//! 4. [`Mdec::decode_finish`] confirms DMA0 drained.
21//!
22//! Every wait is bounded (`psx_io::dma::wait_done`), and every kick aborts
23//! the channel first, per the SDK rule for silicon DMA wedges.
24//!
25//! # Reset latency
26//!
27//! On a PAL SCPH-9002 the MDEC does not finish a reset within the next bus
28//! access: the status read straight after writing the reset still shows the
29//! state from before it (0x6401_0000: busy, data-in full), where the
30//! SuperStation One FPGA and PSoXide already read the documented reset state.
31//! Writing the DMA-request enable right behind the reset, as this driver used
32//! to, left the table upload waiting on DMA0 forever on that console while
33//! both of the others played the movie. So [`Mdec::reset`] waits for the reset to
34//! settle before enabling requests, and [`Mdec::load_tables`] checks that the MDEC
35//! actually raised its data-in request (status bit 28) after each command,
36//! re-writing the enable if it did not, and falls back to CPU writes when
37//! DMA0 still will not take the table.
38
39use psx_hw::mdec::{self as hw, MDEC0, MDEC1};
40use psx_io::dma::{self, Channel};
41use psx_io::periph::MdecDma;
42
43use crate::rle::{self, RleLengthError};
44
45/// DMA block size the MDEC channels use, in words.
46pub const DMA_BLOCK_WORDS: usize = crate::rle::BLOCK_WORDS;
47
48/// Spin budget for one table upload or column transfer.
49pub const DMA_SPINS: u32 = 400_000;
50
51// CHCR: to device / from device, block sync, start.
52const CHCR_IN: u32 =
53    psx_hw::dma::CHCR_TO_DEVICE | psx_hw::dma::CHCR_SYNC_BLOCK | psx_hw::dma::CHCR_START;
54const CHCR_OUT: u32 = psx_hw::dma::CHCR_SYNC_BLOCK | psx_hw::dma::CHCR_START;
55
56/// Standard intra quantization matrix (row-major), DC entry 2.
57const QUANT_ROW_MAJOR: [u8; 64] = [
58    2, 16, 19, 22, 26, 27, 29, 34, //
59    16, 16, 22, 24, 27, 29, 34, 37, //
60    19, 22, 26, 27, 29, 34, 34, 38, //
61    22, 22, 26, 27, 29, 34, 37, 40, //
62    22, 26, 27, 29, 32, 35, 40, 48, //
63    26, 27, 29, 32, 35, 40, 48, 58, //
64    26, 27, 29, 34, 38, 46, 56, 69, //
65    27, 29, 35, 38, 46, 56, 69, 83,
66];
67
68/// Zigzag position `i` to its row-major index.
69const ZIGZAG_TO_ROW_MAJOR: [u8; 64] = [
70    0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, //
71    12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, //
72    35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, //
73    58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
74];
75
76/// Luma then chroma table, zigzag order, as the 32 words command 2 takes.
77pub static QUANT_WORDS: [u32; 32] = build_quant();
78
79const fn build_quant() -> [u32; 32] {
80    let mut bytes = [0u8; 128];
81    let mut i = 0;
82    while i < 64 {
83        let q = QUANT_ROW_MAJOR[ZIGZAG_TO_ROW_MAJOR[i] as usize];
84        bytes[i] = q;
85        bytes[64 + i] = q;
86        i += 1;
87    }
88    let mut words = [0u32; 32];
89    let mut w = 0;
90    while w < 32 {
91        words[w] = u32::from_le_bytes([
92            bytes[4 * w],
93            bytes[4 * w + 1],
94            bytes[4 * w + 2],
95            bytes[4 * w + 3],
96        ]);
97        w += 1;
98    }
99    words
100}
101
102/// The IDCT matrix for MDEC command 3, generated from the DCT basis in
103/// [`crate::idct`].
104pub static SCALE_WORDS: [u32; 32] = crate::idct::MATRIX_WORDS;
105
106// Register constants moved to psx-hw; these forward until games repin.
107
108/// Spin budget for one status wait (reset settle, request, FIFO room).
109/// The console settles a reset in tens of cycles; this is a few ms.
110pub const SETTLE_SPINS: u32 = 20_000;
111/// Status reads to spend after a reset looks settled. A reset from an idle
112/// MDEC can read idle before it has finished, so waiting for busy to drop
113/// is not enough on its own. Each read is an MMIO access, several cycles,
114/// so this is a few hundred cycles against the tens a reset takes.
115const RESET_TAIL_READS: u32 = 64;
116/// Enable writes to try before giving up on the data-in request.
117const ENABLE_ATTEMPTS: u8 = 4;
118
119/// Raw MDEC1 status word.
120#[inline(always)]
121pub fn status() -> u32 {
122    // SAFETY: MDEC status register read.
123    unsafe { psx_io::read_u32(MDEC1) }
124}
125
126/// Wait until `status() & mask == want`. `false` on timeout.
127fn wait_status(mask: u32, want: u32, spins: u32) -> bool {
128    let mut n = 0;
129    while status() & mask != want {
130        if n >= spins {
131            return false;
132        }
133        n += 1;
134    }
135    true
136}
137
138/// Send `blocks` 32-word blocks from `words` to the MDEC over DMA0 without
139/// waiting.
140///
141/// # Safety
142/// `blocks` must not be 0 (the controller reads 0 as 65,536 blocks), and
143/// `blocks * 32` words at `words` must stay alive and unmodified until DMA0
144/// completes.
145unsafe fn dma_in(words: *const u32, blocks: u16) {
146    debug_assert!(blocks != 0, "a zero block count is 65,536 blocks");
147    dma::abort(Channel::MdecIn);
148    // SAFETY: the channel was just aborted, so it is idle; the caller keeps
149    // the `blocks * 32` words at `words` alive and unmodified until DMA0
150    // completes, and `blocks` is not 0.
151    unsafe {
152        dma::start(
153            Channel::MdecIn,
154            dma::Transfer {
155                address: words as u32,
156                size: dma::size_blocks(DMA_BLOCK_WORDS as u16, blocks),
157                control: CHCR_IN,
158            },
159        )
160    };
161}
162
163/// How [`Mdec::load_tables`] got the tables in.
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
165pub struct Tables {
166    /// Enable writes, summed over both commands, before the MDEC raised its
167    /// data-in request: 2 when every first write held. 0 means it never
168    /// asked for data, so DMA0 decodes will not run either.
169    pub enable_writes: u8,
170    /// Tables that went in over the CPU because the MDEC never asked for
171    /// DMA0 data or DMA0 wedged part way (0, 1 or 2).
172    pub cpu_uploads: u8,
173}
174
175impl Tables {
176    /// The MDEC asked for data on both commands, so DMA0 feeds it.
177    pub fn is_dma_ready(&self) -> bool {
178        self.enable_writes != 0 && self.cpu_uploads == 0
179    }
180}
181
182/// The MDEC's driver: it owns the two DMA channels that feed and drain it, so
183/// every decode step that starts one is a method here and the borrow checker
184/// sees a second decoder.
185///
186/// A decode frame is: [`reset`](Self::reset) and
187/// [`load_tables`](Self::load_tables) once, then per frame
188/// [`decode`](Self::decode) with a closure that pulls the output columns.
189///
190/// The status register stays a free function ([`status`]): reading it
191/// cannot disturb a transfer, so it needs no owner.
192#[derive(Debug)]
193#[repr(transparent)]
194pub struct Mdec(MdecDma);
195
196impl Mdec {
197    /// Take the MDEC's DMA channels. Touches no register: call
198    /// [`reset`](Self::reset) next.
199    pub const fn new(dma: MdecDma) -> Self {
200        Self(dma)
201    }
202
203    /// Give the DMA token back.
204    #[inline(always)]
205    pub fn release(self) -> MdecDma {
206        self.0
207    }
208
209    /// Reset the MDEC, wait for the reset to finish, then enable its DMA
210    /// requests on both channels. `false` if the MDEC stayed busy after the
211    /// reset (the enable is still written).
212    pub fn reset(&mut self) -> bool {
213        dma::abort(Channel::MdecIn);
214        dma::abort(Channel::MdecOut);
215        dma::enable_channel(Channel::MdecIn);
216        dma::enable_channel(Channel::MdecOut);
217        // SAFETY: MDEC control register write.
218        unsafe { psx_io::write_u32(MDEC1, hw::CONTROL_RESET) };
219        let settled = wait_status(hw::STATUS_BUSY, 0, SETTLE_SPINS);
220        let mut n = 0;
221        while n < RESET_TAIL_READS {
222            let _ = status();
223            n += 1;
224        }
225        // SAFETY: MDEC control register write.
226        unsafe { psx_io::write_u32(MDEC1, hw::CONTROL_ENABLE_DMA) };
227        settled
228    }
229
230    /// Write `command`, then feed its 32 parameter words: over DMA0 when the
231    /// MDEC asks for data, else over the CPU. Returns (enable writes before the
232    /// request rose, 0 if never; went over the CPU), or `None` if the MDEC
233    /// would not take the words at all.
234    fn upload(&mut self, command: u32, words: &[u32; 32]) -> Option<(u8, bool)> {
235        if !wait_status(hw::STATUS_BUSY, 0, SETTLE_SPINS) {
236            return None;
237        }
238        // SAFETY: MDEC command write.
239        unsafe { psx_io::write_u32(MDEC0, command) };
240        let mut writes = 0u8;
241        let mut asked = wait_status(hw::STATUS_IN_REQUEST, hw::STATUS_IN_REQUEST, SETTLE_SPINS);
242        while !asked && writes + 1 < ENABLE_ATTEMPTS {
243            // The enable can be lost to a reset that had not finished.
244            // SAFETY: MDEC control register write, no reset bit.
245            unsafe { psx_io::write_u32(MDEC1, hw::CONTROL_ENABLE_DMA) };
246            writes += 1;
247            asked = wait_status(hw::STATUS_IN_REQUEST, hw::STATUS_IN_REQUEST, SETTLE_SPINS);
248        }
249        let enable_writes = if asked { writes + 1 } else { 0 };
250        if asked {
251            // SAFETY: `words` is a borrowed table; the transfer is waited out.
252            unsafe { dma_in(words.as_ptr(), 1) };
253            if dma::wait_done(Channel::MdecIn, DMA_SPINS) {
254                return Some((enable_writes, false));
255            }
256            // Part of the table went in: start the command over.
257            dma::abort(Channel::MdecIn);
258            self.reset();
259            if !wait_status(hw::STATUS_BUSY, 0, SETTLE_SPINS) {
260                return None;
261            }
262            // SAFETY: MDEC command write.
263            unsafe { psx_io::write_u32(MDEC0, command) };
264        }
265        for &word in words {
266            if !wait_status(hw::STATUS_IN_FULL, 0, SETTLE_SPINS) {
267                return None;
268            }
269            // SAFETY: MDEC parameter write.
270            unsafe { psx_io::write_u32(MDEC0, word) };
271        }
272        Some((enable_writes, true))
273    }
274
275    /// Upload the standard quantization tables and the IDCT basis. `None` if
276    /// the MDEC would take them neither over DMA0 nor over the CPU.
277    pub fn load_tables(&mut self) -> Option<Tables> {
278        let (quant_writes, quant_cpu) = self.upload(hw::COMMAND_SET_QUANT, &QUANT_WORDS)?;
279        let (scale_writes, scale_cpu) = self.upload(hw::COMMAND_SET_SCALE, &SCALE_WORDS)?;
280        if !wait_status(hw::STATUS_BUSY, 0, SETTLE_SPINS) {
281            return None;
282        }
283        Some(Tables {
284            enable_writes: if quant_writes == 0 || scale_writes == 0 {
285                0
286            } else {
287                quant_writes + scale_writes
288            },
289            cpu_uploads: quant_cpu as u8 + scale_cpu as u8,
290        })
291    }
292
293    /// Upload both tables over CPU writes to MDEC0 only, after
294    /// [`reset`](Self::reset). No DMA involved: the control path for a console
295    /// whose DMA0 will not feed the MDEC. `false` if the input FIFO never made
296    /// room.
297    pub fn load_tables_cpu(&mut self) -> bool {
298        for (command, words) in [
299            (hw::COMMAND_SET_QUANT, &QUANT_WORDS),
300            (hw::COMMAND_SET_SCALE, &SCALE_WORDS),
301        ] {
302            if !wait_status(hw::STATUS_BUSY, 0, SETTLE_SPINS) {
303                return false;
304            }
305            self.write_command(command);
306            for &word in words {
307                if !wait_status(hw::STATUS_IN_FULL, 0, SETTLE_SPINS) {
308                    return false;
309                }
310                self.write_command(word);
311            }
312        }
313        wait_status(hw::STATUS_BUSY, 0, SETTLE_SPINS)
314    }
315
316    /// Write one word to MDEC0: a command, or a parameter the CPU feeds itself.
317    #[inline(always)]
318    pub fn write_command(&mut self, word: u32) {
319        // SAFETY: MDEC command/parameter write.
320        unsafe { psx_io::write_u32(MDEC0, word) }
321    }
322
323    /// Read one word of decoded output from MDEC0 (the CPU path; DMA1 is the
324    /// usual one). Garbage when the output FIFO is empty.
325    #[inline(always)]
326    pub fn read_data(&mut self) -> u32 {
327        // SAFETY: MDEC data read.
328        unsafe { psx_io::read_u32(MDEC0) }
329    }
330
331    /// Start decoding `words` 32-bit words of run-length data (a multiple of 32,
332    /// as [`crate::bitstream::decode_frame`] returns). `mode` is
333    /// [`psx_hw::mdec::DECODE_15BPP`] or [`psx_hw::mdec::DECODE_24BPP`],
334    /// optionally with [`psx_hw::mdec::DECODE_STP`].
335    ///
336    /// [`decode`](Self::decode) is the safe form: it holds the borrow of `rle`
337    /// until DMA0 is done with it.
338    ///
339    /// # Errors
340    /// [`RleLengthError`] when `words` is 0, not a multiple of 32, longer than
341    /// the decode command can announce ([`crate::rle::MAX_WORDS`]) or longer
342    /// than `rle`. Nothing is written to the MDEC then and no DMA starts.
343    ///
344    /// # Safety
345    /// On `Ok`, DMA0 keeps reading `rle` after this returns. The caller must
346    /// keep `rle` alive and unmodified until [`decode_finish`](Self::decode_finish)
347    /// returns, and must call it before the storage is reused or freed.
348    pub unsafe fn decode_start(
349        &mut self,
350        rle: &[u32],
351        words: usize,
352        mode: u32,
353    ) -> Result<(), RleLengthError> {
354        if words > rle.len() {
355            return Err(RleLengthError::PastBuffer);
356        }
357        let blocks = rle::dma_block_count(words)?;
358        // SAFETY: MMIO write to MDEC0, then a DMA0 kick of `blocks` (not 0)
359        // 32-word blocks, exactly the first `words` words of `rle` (checked
360        // above), which the caller keeps alive until `decode_finish`.
361        unsafe {
362            psx_io::write_u32(MDEC0, mode | words as u32);
363            dma_in(rle.as_ptr(), blocks);
364        }
365        Ok(())
366    }
367
368    /// Decode all of `rle` (a multiple of 32 words, as
369    /// [`crate::bitstream::decode_frame`] returns) with DMA0 feeding the MDEC,
370    /// the safe form of [`decode_start`](Self::decode_start).
371    ///
372    /// `columns` runs while DMA0 feeds the MDEC and pulls the output, normally
373    /// with [`read_column`](Self::read_column) once per column; it gets the
374    /// driver back for that. Returns its result and
375    /// [`decode_finish`](Self::decode_finish)'s: `false` there means DMA0 was
376    /// still busy and has been aborted.
377    ///
378    /// # Errors
379    /// [`RleLengthError`] when `rle` is empty, not a whole number of 32-word
380    /// blocks or longer than [`crate::rle::MAX_WORDS`]; `columns` does not run.
381    pub fn decode<R>(
382        &mut self,
383        rle: &[u32],
384        mode: u32,
385        columns: impl FnOnce(&mut Self) -> R,
386    ) -> Result<(R, bool), RleLengthError> {
387        // Finishes the decode on every exit, an unwinding `columns` included.
388        struct Finish<'a> {
389            mdec: &'a mut Mdec,
390            done: bool,
391        }
392        impl Drop for Finish<'_> {
393            fn drop(&mut self) {
394                if !self.done {
395                    self.mdec.decode_finish();
396                }
397            }
398        }
399        // SAFETY: `rle` stays borrowed until this function returns. On `Err`
400        // no DMA started; on `Ok` every return path runs decode_finish first
401        // (directly, or via `Finish` when unwinding), which returns only once
402        // DMA0 is done or aborted.
403        unsafe { self.decode_start(rle, rle.len(), mode)? };
404        let mut finish = Finish {
405            mdec: self,
406            done: false,
407        };
408        let out = columns(finish.mdec);
409        finish.done = true;
410        Ok((out, finish.mdec.decode_finish()))
411    }
412
413    /// Start pulling the next `words` words (a multiple of 32) of decoded
414    /// pixels over DMA1 into `dst` and return at once;
415    /// [`column_done`](Self::column_done) says when they are in. The MDEC
416    /// decodes only while DMA1 drains its output, so a player that overlaps
417    /// work with the decode kicks the next column as soon as the last one
418    /// lands.
419    ///
420    /// Only whole 32-word blocks move. Returns `false`, having started
421    /// nothing, when that is no block at all or more than one transfer can
422    /// count: the controller reads a zero block count as 65,536 blocks, which
423    /// would run far past `dst`.
424    ///
425    /// # Safety
426    /// `dst` must point at `words` writable words that stay alive and
427    /// untouched until [`column_done`](Self::column_done) returns `true` or
428    /// DMA1 is aborted ([`abort_decode`](Self::abort_decode)).
429    pub unsafe fn read_column_start(&mut self, dst: *mut u32, words: usize) -> bool {
430        let Ok(blocks) = u16::try_from(words / DMA_BLOCK_WORDS) else {
431            return false;
432        };
433        if blocks == 0 {
434            return false;
435        }
436        dma::abort(Channel::MdecOut);
437        // SAFETY: the channel was just aborted, so it is idle. The transfer
438        // writes `blocks * DMA_BLOCK_WORDS` = `words` words at `dst`, which
439        // the caller keeps writable and untouched until it completes or is
440        // aborted.
441        unsafe {
442            dma::start(
443                Channel::MdecOut,
444                dma::Transfer {
445                    address: dst as u32,
446                    size: dma::size_blocks(DMA_BLOCK_WORDS as u16, blocks),
447                    control: CHCR_OUT,
448                },
449            )
450        };
451        true
452    }
453
454    /// True once the column [`read_column_start`](Self::read_column_start)
455    /// asked for has arrived.
456    #[inline]
457    pub fn column_done(&self) -> bool {
458        !dma::is_busy(Channel::MdecOut)
459    }
460
461    /// Stop both MDEC DMA channels, e.g. when a decode has taken too long. A
462    /// column or run-length buffer a transfer was using is free again once
463    /// this returns.
464    pub fn abort_decode(&mut self) {
465        dma::abort(Channel::MdecOut);
466        dma::abort(Channel::MdecIn);
467    }
468
469    /// Pull the next `dst.len()` words (a multiple of 32) of decoded pixels
470    /// over DMA1 and wait for them. For 15bpp one 16-pixel-wide column of
471    /// height `h` is `8 * h` words. `false` if DMA1 wedged.
472    pub fn read_column(&mut self, dst: &mut [u32]) -> bool {
473        // SAFETY: `dst` is borrowed exclusively until this function returns;
474        // the wait below, or the abort on a wedge, ends the transfer before
475        // then.
476        if !unsafe { self.read_column_start(dst.as_mut_ptr(), dst.len()) } {
477            return false;
478        }
479        dma::wait_or_abort(Channel::MdecOut, DMA_SPINS)
480    }
481
482    /// Confirm DMA0 finished feeding the frame. `false` (after aborting the
483    /// channel) if it is still busy, e.g. the frame held more data than was
484    /// read back.
485    pub fn decode_finish(&mut self) -> bool {
486        dma::wait_or_abort(Channel::MdecIn, DMA_SPINS)
487    }
488}
489
490/// A driver for a deprecated forwarder that never took the token.
491fn steal_mdec() -> Mdec {
492    // SAFETY: a token is a logic guard, not a memory-safety one (see
493    // `psx_io::periph`), and the old free functions never took one.
494    Mdec::new(unsafe { MdecDma::steal() })
495}
496
497/// Reset the MDEC and enable its DMA requests.
498#[deprecated(note = "use `Mdec::reset` with the `MdecDma` token")]
499pub fn reset() -> bool {
500    steal_mdec().reset()
501}
502
503/// Upload the standard quantization tables and the IDCT basis.
504#[deprecated(note = "use `Mdec::load_tables` with the `MdecDma` token")]
505pub fn load_tables() -> Option<Tables> {
506    steal_mdec().load_tables()
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn the_driver_is_its_token_and_gives_it_back() {
515        assert_eq!(core::mem::size_of::<Mdec>(), 0);
516        // SAFETY: a test-local token on the host; `new` touches no register.
517        let mdec = Mdec::new(unsafe { MdecDma::steal() });
518        let _token: MdecDma = mdec.release();
519    }
520
521    #[test]
522    fn a_decode_that_cannot_be_described_never_runs_its_columns() {
523        // SAFETY: a test-local token on the host; both calls are refused
524        // before any register is touched.
525        let mut mdec = Mdec::new(unsafe { MdecDma::steal() });
526        let mut ran = false;
527        // Empty, and one word short of a block: refused before any write.
528        for rle in [&[][..], &[0u32; 31][..]] {
529            let result = mdec.decode(rle, hw::DECODE_15BPP, |_| ran = true);
530            assert!(result.is_err());
531        }
532        assert!(!ran);
533    }
534
535    #[test]
536    fn a_column_slice_the_channel_cannot_describe_is_refused_before_it_starts() {
537        // SAFETY: a test-local token on the host; both are refused first.
538        let mut mdec = Mdec::new(unsafe { MdecDma::steal() });
539        assert!(!mdec.read_column(&mut []));
540        assert!(!mdec.read_column(&mut [0u32; 31]));
541    }
542}