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

psx_gpu/
chain.rs

1//! The raw linked-list layer: DMA channel 2 walking a chain of GP0 nodes.
2//!
3//! Each node is a tag word, the next node's address in bits 23..=0
4//! ([`LIST_END`] ends the list) and the payload word count in bits 31..=24,
5//! followed by that many GP0 words. The DMA controller follows the links on
6//! its own, so whatever a tag names is read with no regard for Rust's
7//! borrows: that is why [`submit_raw`] and [`submit_async_raw`] are
8//! `unsafe`. The safe layers above them prove their contract instead:
9//! [`crate::frame`] with lifetimes, [`StaticChain`] with `'static`
10//! immutability, and [`crate::ordered`] with `'static` storage it owns.
11//!
12//! Every wait here is bounded. A walk that outlives its budget is aborted
13//! and the GPU's half-read command discarded, so a wedged channel costs a
14//! reset instead of a hang; [`crate::recovery_stats`] counts the aborts.
15
16use psx_hw::gpu::{gp0, gp1, DmaDirection};
17use psx_io::dma::{self, Channel};
18use psx_io::gpu::write_display_control;
19use psx_io::periph::GpuDma;
20
21/// The link value that ends a list.
22pub const LIST_END: u32 = psx_hw::dma::linked_list::END;
23
24/// Most payload words one linked-list DMA node may carry (the words after
25/// its tag), the depth of the GPU's command FIFO.
26///
27/// Silicon very likely loses words from longer nodes while it draws.
28/// Hardware-tests v1.24 drew the same 16 half-screen Gouraud triangles as
29/// 16 nodes and as 4 nodes of 24 words: the packed list's closing GP0(1Fh)
30/// arrived at 314,075 clocks against 625,348, about 8 triangles' worth at
31/// the 39,084 clocks each costs, and its last-node drain matched the
32/// unpacked list's one-triangle gap, so roughly half its drawing never
33/// happened. Every SDK node builder stays at or under this limit:
34/// [`crate::ordered::OrderedCommandStream`] caps nodes at
35/// [`crate::ordered::NODE_PAYLOAD_WORDS`], the frame adds refuse longer
36/// packets, and every [`crate::prim`] packet is shorter.
37pub const MAX_NODE_WORDS: usize = 16;
38
39/// A linked-list DMA node holding only GP0(1Fh), for the end of a chain.
40///
41/// Link it as the chain's last node and the GPU raises
42/// [`crate::is_draw_done`] when it gets there. It is immutable and shared:
43/// every chain can end on [`DRAW_DONE_NODE`].
44#[derive(Debug)]
45#[repr(C, align(4))]
46pub struct DrawDoneNode([u32; 2]);
47
48impl DrawDoneNode {
49    /// The node's tag word, the address a chain links to.
50    #[inline]
51    pub fn as_ptr(&self) -> *const u32 {
52        self.0.as_ptr()
53    }
54}
55
56/// The shared GP0(1Fh) node: one payload word, then the end of the list.
57pub static DRAW_DONE_NODE: DrawDoneNode = DrawDoneNode([(1 << 24) | LIST_END, gp0::REQUEST_IRQ]);
58
59/// One immutable linked-list node: `W` GP0 words, then the end of the list.
60///
61/// Built in a `static`, it is a chain that stays valid for the whole run, so
62/// [`crate::Gpu::submit_static`] can kick it from safe code without waiting.
63///
64/// ```
65/// use psx_gpu::chain::StaticPacket;
66/// // GP0(E1h) draw mode, then GP0(1Fh).
67/// static MODE_THEN_IRQ: StaticPacket<2> = StaticPacket::new([0xE100_0000, 0x1F00_0000]);
68/// assert_eq!(MODE_THEN_IRQ.words(), &[0xE100_0000, 0x1F00_0000]);
69/// ```
70#[derive(Debug)]
71#[repr(C, align(4))]
72pub struct StaticPacket<const W: usize> {
73    tag: u32,
74    words: [u32; W],
75}
76
77impl<const W: usize> StaticPacket<W> {
78    /// A node carrying `words` that ends the list.
79    pub const fn new(words: [u32; W]) -> Self {
80        const { assert!(W <= MAX_NODE_WORDS, "packet longer than one GPU DMA node") };
81        Self {
82            tag: ((W as u32) << 24) | LIST_END,
83            words,
84        }
85    }
86
87    /// The payload words.
88    pub const fn words(&self) -> &[u32; W] {
89        &self.words
90    }
91
92    /// The node's tag word, the address a chain links to.
93    #[inline]
94    pub fn as_ptr(&self) -> *const u32 {
95        // From the whole node, so the walk may read the payload after it.
96        core::ptr::from_ref(self).cast::<u32>()
97    }
98}
99
100mod sealed {
101    pub trait Sealed {}
102}
103
104/// A complete, immutable linked list: [`StaticPacket`] or [`DrawDoneNode`].
105///
106/// Sealed: implementors guarantee that a shared reference to them is a
107/// whole chain whose nodes never change while the reference lives.
108pub trait StaticChain: sealed::Sealed {
109    /// Address of the first node's tag.
110    fn head(&self) -> *const u32;
111}
112
113impl sealed::Sealed for DrawDoneNode {}
114impl StaticChain for DrawDoneNode {
115    #[inline]
116    fn head(&self) -> *const u32 {
117        self.as_ptr()
118    }
119}
120
121impl<const W: usize> sealed::Sealed for StaticPacket<W> {}
122impl<const W: usize> StaticChain for StaticPacket<W> {
123    #[inline]
124    fn head(&self) -> *const u32 {
125        self.as_ptr()
126    }
127}
128
129/// Kick the chain at `head` and return while the GPU walks it.
130///
131/// A walk already running on the channel is waited out first (or aborted
132/// if it wedged), so the CPU can build the next frame while this one is
133/// drawn. Pair it with [`wait`].
134///
135/// # Safety
136///
137/// `head` must point at a 4-byte-aligned node tag in main RAM. Every node
138/// reachable from it must have at most [`MAX_NODE_WORDS`] payload words and
139/// lie in main RAM, and must stay live and unmodified until [`wait`]
140/// returns (or a later kick, which waits for this walk first).
141#[doc(alias = "DrawOTag")]
142#[inline]
143pub unsafe fn submit_async_raw(_dma: &mut GpuDma, head: *const u32) {
144    // SAFETY: forwarded contract; the token's borrow is the caller's proof
145    // that no other code drives the channel.
146    unsafe { start_walk(head) }
147}
148
149/// Kick the chain at `head` and wait until it has been walked.
150///
151/// # Safety
152///
153/// As [`submit_async_raw`], for the duration of this call.
154#[doc(alias = "DrawOTag")]
155#[inline]
156pub unsafe fn submit_raw(dma: &mut GpuDma, head: *const u32) {
157    // SAFETY: forwarded contract; the wait ends the walk before return.
158    unsafe { submit_async_raw(dma, head) };
159    wait(dma);
160}
161
162/// Wait until the walk kicked last has finished reading its chain.
163///
164/// This is the CPU-blocked-on-GPU part of a submission; profiling code
165/// times it apart from the kick to split GPU-draw cost from CPU build cost.
166/// The GPU may still be drawing the last packets; the chain's memory is
167/// free once this returns.
168#[inline]
169pub fn wait(_dma: &mut GpuDma) {
170    wait_walk();
171}
172
173/// [`submit_async_raw`]'s body, for callers that hold the token elsewhere.
174///
175/// # Safety
176///
177/// As [`submit_async_raw`].
178pub(crate) unsafe fn start_walk(head: *const u32) {
179    // A finished walk does not mean the GPU has drawn what it read. Do not
180    // wait for idle here: channel 2's request handshake queues the next list
181    // behind that work, which is how PsyQ and PSn00bSDK keep the GPU fed.
182    // Only the channel and the list's storage must be free.
183    if !dma::wait_done(Channel::Gpu, dma::DEFAULT_SPINS) {
184        abort_wedged_walk();
185    }
186
187    // Games re-route DMA for VRAM readback and do not always restore it, so
188    // point it at GP0 before every kick.
189    write_display_control(gp1::dma_direction(DmaDirection::CpuToGp0 as u32));
190    dma::enable_channel(Channel::Gpu);
191    // SAFETY: the channel is idle (waited out or aborted above); the caller
192    // keeps the chain live and unmodified until the walk is waited out.
193    // `dma::start` publishes the payload and tag stores before the CHCR
194    // store.
195    unsafe {
196        dma::start(
197            Channel::Gpu,
198            dma::Transfer {
199                address: head.expose_provenance() as u32,
200                // Linked-list mode ignores BCR, but hardware wants it written.
201                size: dma::size_words(0),
202                control: psx_hw::dma::CHCR_TO_DEVICE
203                    | psx_hw::dma::CHCR_SYNC_LINKED
204                    | psx_hw::dma::CHCR_START,
205            },
206        )
207    };
208}
209
210/// [`wait`]'s body, for callers that hold the token elsewhere.
211#[inline]
212pub(crate) fn wait_walk() {
213    if !dma::wait_done(Channel::Gpu, dma::DEFAULT_SPINS) {
214        abort_wedged_walk();
215    }
216    // Keep the caller's buffer-reuse stores after the completion read (or
217    // the abort).
218    dma::compiler_barrier();
219}
220
221/// Stop a channel-2 walk that outlived its spin budget.
222///
223/// The walker stopped mid-packet, so the GPU still waits for the rest of a
224/// command: GP1(01h) discards it, or every later ready-wait blocks on a GPU
225/// that can never become ready. The reset goes past the present-queue
226/// direct-access guard, which would wait on the walk that just wedged; the
227/// paired-arena fence reaches this from a scratchpad stack that stack-guard
228/// bounds only through direct calls, hence `inline(always)`.
229#[inline(always)]
230fn abort_wedged_walk() {
231    dma::abort(Channel::Gpu);
232    psx_io::gpu::write_display_control_unguarded(gp1::RESET_CMD_BUFFER);
233    // SAFETY: volatile aligned accesses to a private static that only this
234    // function writes; the program is single threaded and no handler
235    // touches it.
236    unsafe {
237        let count = core::ptr::addr_of_mut!(DMA_ABORTS);
238        count.write_volatile(count.read_volatile().wrapping_add(1));
239    }
240}
241
242/// Walks [`abort_wedged_walk`] has stopped since boot.
243static mut DMA_ABORTS: u32 = 0;
244
245/// Walks aborted since boot.
246pub(crate) fn dma_abort_count() -> u32 {
247    // SAFETY: a volatile aligned read of a private static.
248    unsafe { core::ptr::addr_of!(DMA_ABORTS).read_volatile() }
249}