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

psx_gpu/
gpu.rs

1//! The GPU driver.
2
3use crate::chain::StaticChain;
4use crate::display::DisplayConfig;
5use crate::material::{TextureMaterial, TextureWindow};
6use crate::prim::GpuPacket;
7use psx_hw::gpu::{gp0, gp1, DmaDirection};
8use psx_io::gpu::{wait_command_ready, write_command, write_display_control};
9use psx_io::periph::GpuDma;
10
11bitflags::bitflags! {
12    /// GP0(E6h) mask-bit state.
13    ///
14    /// With both flags set the GPU gives front-to-back occlusion without a
15    /// Z-buffer: draw nearest first, and farther pixels that would overdraw
16    /// are rejected. The state applies until changed, so return to
17    /// [`MaskMode::empty`] before translucent passes that must not mask.
18    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19    pub struct MaskMode: u32 {
20        /// Force bit 15 on every pixel written.
21        const SET_ON_DRAW = 1 << 0;
22        /// Skip pixels whose bit 15 is already set.
23        const CHECK_BEFORE_DRAW = 1 << 1;
24    }
25}
26
27/// The PS1 GPU: its command port (GP0), its control port (GP1) and DMA
28/// channel 2, which feeds GP0.
29///
30/// `Gpu` owns the [`GpuDma`] token, so every operation that writes a GPU
31/// port is a method that needs `&mut Gpu`. That puts the borrow checker
32/// between two users of one command stream: drawing immediately while a
33/// linked-list walk feeds GP0 is a compile error, not a corrupted frame.
34///
35/// ```compile_fail,E0500
36/// # use psx_gpu::{ot::OrderingTable, prim::TriFlat, Gpu};
37/// # fn f(gpu: &mut Gpu, ot: &mut OrderingTable<8>) {
38/// let frame = ot.frame();
39/// frame.submit_with(gpu.dma_mut(), || {
40///     gpu.draw(&TriFlat::new([(0, 0), (8, 0), (0, 8)], 255, 0, 0)); // GP0 mid-walk
41/// });
42/// # }
43/// ```
44///
45/// It is zero-sized, so holding and passing it costs nothing. Reading
46/// status (`psx_gpu::is_draw_done`, `psx_io::gpu::status`) needs no
47/// handle: a read can't disturb the command stream.
48///
49/// ```no_run
50/// use psx_gpu::display::{DisplayConfig, Resolution, VideoMode};
51/// use psx_gpu::prim::FillRect;
52/// use psx_gpu::Gpu;
53///
54/// fn start(dma: psx_io::periph::GpuDma) -> Gpu {
55///     let mut gpu = Gpu::new(dma, DisplayConfig::new(VideoMode::Ntsc, Resolution::R320X240));
56///     gpu.draw(&FillRect::new((0, 0), (320, 240), (0, 0, 32)));
57///     gpu
58/// }
59/// ```
60#[derive(Debug)]
61#[repr(transparent)]
62pub struct Gpu(GpuDma);
63
64impl Gpu {
65    /// Take over the GPU: reset it, program `display`, point DMA at GP0 and
66    /// turn the display on.
67    #[doc(alias = "ResetGraph")]
68    pub fn new(dma: GpuDma, display: DisplayConfig) -> Self {
69        reset(display);
70        Self(dma)
71    }
72
73    /// The driver as a view of a token held elsewhere, such as the one a
74    /// `FramePair` owns, for immediate drawing between its frames.
75    #[inline(always)]
76    pub fn from_dma_mut(dma: &mut GpuDma) -> &mut Self {
77        // SAFETY: `Gpu` is a `repr(transparent)` wrapper of `GpuDma`, so the
78        // two types have the same layout and the cast keeps the exclusive
79        // borrow.
80        unsafe { &mut *core::ptr::from_mut(dma).cast::<Self>() }
81    }
82
83    /// The token, for APIs that start channel 2 themselves (frame
84    /// submission, VRAM DMA). The driver stays borrowed meanwhile.
85    #[inline(always)]
86    pub fn dma_mut(&mut self) -> &mut GpuDma {
87        &mut self.0
88    }
89
90    /// Give the token back.
91    #[inline(always)]
92    pub fn release(self) -> GpuDma {
93        self.0
94    }
95
96    /// Reprogram the display mode and the picture position.
97    #[doc(alias = "PutDispEnv")]
98    pub fn set_display(&mut self, display: DisplayConfig) {
99        write_display_control(display.mode_command());
100        write_display_window(display);
101    }
102
103    /// Show VRAM from `origin` (GP1(05h)): a double-buffer flip.
104    #[inline]
105    pub fn set_display_start(&mut self, origin: (u16, u16)) {
106        write_display_control(gp1::display_start(origin.0 as u32, origin.1 as u32));
107    }
108
109    /// Turn the video output on or off (GP1(03h)).
110    #[doc(alias = "SetDispMask")]
111    #[inline]
112    pub fn set_display_enabled(&mut self, enabled: bool) {
113        write_display_control(gp1::display_enable(enabled));
114    }
115
116    /// Clip drawing to the rectangle from `top_left` to `bottom_right`
117    /// (inclusive VRAM coordinates).
118    #[inline]
119    pub fn set_draw_area(&mut self, top_left: (u16, u16), bottom_right: (u16, u16)) {
120        set_draw_area(top_left, bottom_right);
121    }
122
123    /// Add `offset` to every vertex drawn from now on (GP0(E5h)), so a
124    /// back buffer can be drawn in its own coordinates.
125    #[inline]
126    pub fn set_draw_offset(&mut self, offset: (i16, i16)) {
127        set_draw_offset(offset);
128    }
129
130    /// Set the mask-bit state (GP0(E6h)).
131    #[inline]
132    pub fn set_mask_mode(&mut self, mode: MaskMode) {
133        wait_command_ready();
134        write_command(gp0::mask_bit(
135            mode.contains(MaskMode::SET_ON_DRAW),
136            mode.contains(MaskMode::CHECK_BEFORE_DRAW),
137        ));
138    }
139
140    /// Apply `material`'s texture page, blend equation, dither and
141    /// texture window (GP0(E1h), GP0(E2h)).
142    #[inline]
143    pub fn set_draw_mode(&mut self, material: TextureMaterial) {
144        set_draw_mode(material);
145    }
146
147    /// Set the texture window (GP0(E2h)).
148    #[inline]
149    pub fn set_texture_window(&mut self, window: TextureWindow) {
150        wait_command_ready();
151        write_command(window.word());
152    }
153
154    /// Send `packet` to GP0 now, without an ordering table.
155    ///
156    /// The same packet types go into an `OtFrame`, so a primitive has one
157    /// encoder whether it is drawn now or linked into a frame.
158    #[inline(always)]
159    pub fn draw<P: GpuPacket>(&mut self, packet: &P) {
160        draw(packet);
161    }
162
163    /// Wait until the GPU has drawn everything sent to it.
164    ///
165    /// See [`crate::wait_idle`] for what that waits on.
166    #[doc(alias = "DrawSync")]
167    #[inline]
168    pub fn wait_idle(&mut self) {
169        crate::wait_idle_impl();
170    }
171
172    /// Clear GPUSTAT bit 24 (GP1(02h)) before kicking work that ends in
173    /// GP0(1Fh); see `psx_gpu::is_draw_done`.
174    #[inline]
175    pub fn arm_draw_done(&mut self) {
176        write_display_control(gp1::ACK_IRQ);
177    }
178
179    /// Send GP0(1Fh), closing immediate drawing so `is_draw_done` rises
180    /// once it is drawn.
181    #[inline]
182    pub fn signal_draw_done(&mut self) {
183        wait_command_ready();
184        write_command(gp0::REQUEST_IRQ);
185    }
186
187    /// Kick a `'static`, immutable chain, such as
188    /// [`DRAW_DONE_NODE`](crate::chain::DRAW_DONE_NODE), without waiting for it.
189    ///
190    /// The chain outlives any walk, so there is nothing to wait for before
191    /// reusing memory; a later kick waits for this walk on its own.
192    #[inline]
193    pub fn submit_static(&mut self, chain: &'static impl StaticChain) {
194        // SAFETY: `StaticChain` guarantees a well-formed list that stays live
195        // and unmodified for 'static.
196        unsafe { crate::chain::start_walk(chain.head()) }
197    }
198}
199
200/// [`Gpu::new`]'s port writes.
201pub(crate) fn reset(display: DisplayConfig) {
202    write_display_control(gp1::RESET);
203    write_display_control(display.mode_command());
204    write_display_window(display);
205    write_display_control(gp1::dma_direction(DmaDirection::CpuToGp0 as u32));
206    write_display_control(gp1::display_enable(true));
207}
208
209/// GP1(06h) and GP1(07h) for `display`.
210pub(crate) fn write_display_window(display: DisplayConfig) {
211    write_display_control(display.horizontal_range_command());
212    write_display_control(display.vertical_range_command());
213}
214
215pub(crate) fn set_draw_area(top_left: (u16, u16), bottom_right: (u16, u16)) {
216    wait_command_ready();
217    write_command(gp0::draw_area_top_left(
218        top_left.0 as u32,
219        top_left.1 as u32,
220    ));
221    write_command(gp0::draw_area_bottom_right(
222        bottom_right.0 as u32,
223        bottom_right.1 as u32,
224    ));
225}
226
227pub(crate) fn set_draw_offset(offset: (i16, i16)) {
228    wait_command_ready();
229    write_command(gp0::draw_offset(offset.0 as i32, offset.1 as i32));
230}
231
232pub(crate) fn set_draw_mode(material: TextureMaterial) {
233    wait_command_ready();
234    write_command(material.draw_mode_word());
235    write_command(material.texture_window_word());
236}
237
238/// One ready-wait, then the packet's payload words, in order.
239#[inline(always)]
240pub(crate) fn draw<P: GpuPacket>(packet: &P) {
241    let words = core::ptr::from_ref(packet).cast::<u32>();
242    wait_command_ready();
243    let mut index = 1;
244    while index <= P::WORDS as usize {
245        // SAFETY: `GpuPacket` guarantees a `repr(C)` layout of a tag word and
246        // then at least `WORDS` initialised `u32`s, so words 1..=WORDS are
247        // inside `packet`, which the borrow keeps alive and unchanged.
248        write_command(unsafe { words.add(index).read() });
249        index += 1;
250    }
251}
252
253/// Draw area and offset for a buffer at VRAM line `top`, written without a
254/// ready-wait, as `FrameBuffer` always has: state commands between frames.
255pub(crate) fn write_draw_target(top: u16, width: u16, height: u16) {
256    write_command(gp0::draw_area_top_left(0, top as u32));
257    write_command(gp0::draw_area_bottom_right(
258        (width - 1) as u32,
259        (top + height - 1) as u32,
260    ));
261    write_command(gp0::draw_offset(0, top as i32));
262}