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

psx_gpu/
framebuf.rs

1//! The pre-`Gpu` double buffer, kept for one stage: use
2//! [`crate::display::DoubleBuffer`], whose methods take the [`crate::Gpu`]
3//! they write to.
4//!
5//! A `FrameBuffer` tracks two on-screen regions in VRAM -- one being
6//! displayed, one being drawn into. `swap()` flips them at a VBlank
7//! boundary. Layout is vertical: buffer A at Y=0, buffer B at Y=height.
8//! That fits 2×(640×240) side-by-side inside the 1024×512 VRAM on
9//! standard NTSC resolutions and gives the engine a natural tear-free
10//! presentation.
11#![allow(deprecated, reason = "the module implements the deprecated type")]
12
13use psx_hw::gpu::{gp0, gp1};
14use psx_io::gpu::{write_command, write_display_control};
15
16/// Tracks display-start between two vertically stacked buffers.
17#[deprecated(note = "use `psx_gpu::display::DoubleBuffer`, whose methods take the `Gpu`")]
18#[derive(Debug)]
19pub struct FrameBuffer {
20    /// Display width in pixels (set by [`FrameBuffer::new`]).
21    pub width: u16,
22    /// Display height in pixels.
23    pub height: u16,
24    /// Vertical distance in VRAM rows between the two buffers.
25    pub stride: u16,
26    /// Index (0 or 1) of the buffer currently drawn TO.
27    pub drawing: u8,
28}
29
30impl FrameBuffer {
31    /// Create a framebuffer for the given active display size. Buffer
32    /// A lives at VRAM Y=0, buffer B at Y=`height`.
33    pub const fn new(width: u16, height: u16) -> Self {
34        Self {
35            width,
36            height,
37            stride: height,
38            drawing: 0,
39        }
40    }
41
42    /// Create vertically stacked buffers with an explicit row stride.
43    ///
44    /// This supports layouts which reserve rows between the visible buffers,
45    /// such as a 320x240 display at Y=0 and Y=256 with palettes in the gap.
46    /// A stride smaller than `height` is clamped so the buffers cannot overlap.
47    pub const fn new_strided(width: u16, height: u16, stride: u16) -> Self {
48        Self {
49            width,
50            height,
51            stride: if stride < height { height } else { stride },
52            drawing: 0,
53        }
54    }
55
56    /// Y-coordinate of buffer `idx` in VRAM.
57    #[inline]
58    pub const fn buffer_y(&self, idx: u8) -> u16 {
59        if idx == 0 {
60            0
61        } else {
62            self.stride
63        }
64    }
65
66    /// Push a display-start command for the buffer we're NOT currently
67    /// drawing to -- flipping the display at the next VBlank.
68    pub fn swap(&mut self) {
69        // Show the buffer we were drawing into; drain into the other.
70        let show = self.drawing;
71        self.drawing ^= 1;
72        let show_y = self.buffer_y(show);
73        write_display_control(gp1::display_start(0, show_y as u32));
74
75        // Re-set the draw-area / draw-offset to match the new target buffer.
76        let target_y = self.buffer_y(self.drawing);
77        write_command(gp0::draw_area_top_left(0, target_y as u32));
78        write_command(gp0::draw_area_bottom_right(
79            (self.width - 1) as u32,
80            (target_y + self.height - 1) as u32,
81        ));
82        write_command(gp0::draw_offset(0, target_y as i32));
83    }
84
85    /// Deferred-present half of [`FrameBuffer::swap`]: switch the DRAW side
86    /// now and return the GP1 display-start word for the finished buffer,
87    /// for the caller to apply exactly at a blank edge (e.g. via psx-rt's
88    /// queued-GP1 VBlank hook). Drawing must be finished
89    /// ([`crate::wait_idle`]) before this call, because it rewrites the draw
90    /// area/offset directly.
91    pub fn begin_swap(&mut self) -> u32 {
92        let display_start = self.begin_deferred_swap();
93        self.apply_draw_target();
94        display_start
95    }
96
97    /// Select the next draw buffer without writing any GP0 commands.
98    ///
99    /// This is the non-blocking first half of a pipelined swap. Queue the
100    /// returned GP1 word for a VBlank edge whose handler applies it only once
101    /// the frame's closing GP0(1Fh) has run (psx-rt's
102    /// `interrupts::queue_display_control_at_vblank`; see [`crate::is_draw_done`]), wait
103    /// until that queue entry is consumed, then call
104    /// [`FrameBuffer::apply_draw_target`] before clearing or drawing into the
105    /// newly selected buffer.
106    ///
107    /// Unlike [`FrameBuffer::begin_swap`], this method is safe to call while
108    /// the previous frame is still rasterising: it only changes CPU-owned
109    /// bookkeeping and does not touch the GPU command port.
110    pub fn begin_deferred_swap(&mut self) -> u32 {
111        let show = self.drawing;
112        self.drawing ^= 1;
113        gp1::display_start(0, self.buffer_y(show) as u32)
114    }
115
116    /// Program GP0 draw area and offset for the currently selected draw side.
117    ///
118    /// Call this after a display-start queued by
119    /// [`FrameBuffer::begin_deferred_swap`] has been applied at a VBlank edge
120    /// that found the frame drawn. It deliberately remains separate from buffer selection so
121    /// a busy raster never turns these three state writes into a synchronous
122    /// CPU wait at the start of the next frame.
123    pub fn apply_draw_target(&self) {
124        let target_y = self.buffer_y(self.drawing);
125        write_command(gp0::draw_area_top_left(0, target_y as u32));
126        write_command(gp0::draw_area_bottom_right(
127            (self.width - 1) as u32,
128            (target_y + self.height - 1) as u32,
129        ));
130        write_command(gp0::draw_offset(0, target_y as i32));
131    }
132
133    /// Clear the back-buffer (the one currently being drawn to) to `(r, g, b)`.
134    pub fn clear(&self, r: u8, g: u8, b: u8) {
135        crate::gpu::draw(&crate::prim::FillRect::new(
136            (0, self.buffer_y(self.drawing)),
137            (self.width, self.height),
138            (r, g, b),
139        ));
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn default_layout_stacks_buffers_at_visible_height() {
149        let framebuffer = FrameBuffer::new(320, 240);
150        assert_eq!(framebuffer.buffer_y(0), 0);
151        assert_eq!(framebuffer.buffer_y(1), 240);
152    }
153
154    #[test]
155    fn strided_layout_preserves_reserved_vram_rows() {
156        let framebuffer = FrameBuffer::new_strided(320, 240, 256);
157        assert_eq!(framebuffer.buffer_y(0), 0);
158        assert_eq!(framebuffer.buffer_y(1), 256);
159        assert_eq!(framebuffer.height, 240);
160    }
161
162    #[test]
163    fn deferred_swap_selects_the_opposite_buffer_without_gpu_state() {
164        let mut framebuffer = FrameBuffer::new_strided(320, 240, 256);
165
166        let first_display = framebuffer.begin_deferred_swap();
167        assert_eq!(first_display, gp1::display_start(0, 0));
168        assert_eq!(framebuffer.drawing, 1);
169
170        let second_display = framebuffer.begin_deferred_swap();
171        assert_eq!(second_display, gp1::display_start(0, 256));
172        assert_eq!(framebuffer.drawing, 0);
173    }
174
175    #[test]
176    fn strided_layout_prevents_buffer_overlap() {
177        let framebuffer = FrameBuffer::new_strided(320, 240, 128);
178        assert_eq!(framebuffer.stride, 240);
179    }
180}