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}