1use psx_hw::gpu::gp1;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum VideoMode {
14 Ntsc,
16 Pal,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub struct Resolution {
26 width: u16,
27 height: u16,
28}
29
30impl Resolution {
31 pub const R320X240: Self = Self::new(320, 240);
33 pub const R256X240: Self = Self::new(256, 240);
35 pub const R512X240: Self = Self::new(512, 240);
37 pub const R640X240: Self = Self::new(640, 240);
39 pub const R320X256: Self = Self::new(320, 256);
41 pub const R640X480: Self = Self::new(640, 480);
43
44 const fn new(width: u16, height: u16) -> Self {
45 Self { width, height }
46 }
47
48 pub const fn width(self) -> u16 {
50 self.width
51 }
52
53 pub const fn height(self) -> u16 {
55 self.height
56 }
57
58 pub const fn is_interlaced(self) -> bool {
60 self.height >= 480
61 }
62
63 pub(crate) const fn field_lines(self) -> u32 {
67 if self.is_interlaced() {
68 self.height as u32 / 2
69 } else {
70 self.height as u32
71 }
72 }
73
74 pub(crate) const fn clocks_per_pixel(self) -> i32 {
79 match self.width {
80 256 => 10,
81 320 => 8,
82 512 => 5,
83 _ => 4,
84 }
85 }
86
87 const fn horizontal_field(self) -> u32 {
89 match self.width {
90 256 => 0,
91 320 => 1,
92 512 => 2,
93 _ => 3,
94 }
95 }
96}
97
98const H_DISPLAY_WINDOW_START: i32 = 0x260;
101const NTSC_V_DISPLAY_WINDOW_START: i32 = 0x10;
103const PAL_V_DISPLAY_WINDOW_START: i32 = 0x23;
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115pub struct DisplayConfig {
116 pub mode: VideoMode,
118 pub resolution: Resolution,
120 pub offset: (i16, i16),
126}
127
128impl DisplayConfig {
129 pub const fn new(mode: VideoMode, resolution: Resolution) -> Self {
131 Self {
132 mode,
133 resolution,
134 offset: (0, 0),
135 }
136 }
137
138 pub const fn with_offset(self, offset: (i16, i16)) -> Self {
140 Self { offset, ..self }
141 }
142
143 pub(crate) const fn mode_command(self) -> u32 {
148 let interlaced = self.resolution.is_interlaced();
149 gp1::display_mode(
150 self.resolution.horizontal_field(),
151 interlaced as u32,
152 matches!(self.mode, VideoMode::Pal),
153 false,
154 interlaced,
155 )
156 }
157
158 pub(crate) const fn horizontal_range_command(self) -> u32 {
160 let clocks_per_pixel = self.resolution.clocks_per_pixel();
161 let start = H_DISPLAY_WINDOW_START + self.offset.0 as i32 * clocks_per_pixel;
162 let start = if start < 0 { 0 } else { start as u32 };
163 let end = start + (self.resolution.width as i32 * clocks_per_pixel) as u32;
164 gp1::h_display_range(start, end)
165 }
166
167 pub(crate) const fn vertical_range_command(self) -> u32 {
169 let top = match self.mode {
170 VideoMode::Ntsc => NTSC_V_DISPLAY_WINDOW_START,
171 VideoMode::Pal => PAL_V_DISPLAY_WINDOW_START,
172 };
173 let start = top + self.offset.1 as i32;
174 let start = if start < 0 { 0 } else { start as u32 };
175 gp1::v_display_range(start, start + self.resolution.field_lines())
176 }
177}
178
179const VRAM_LINES: u16 = 512;
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
195pub struct DoubleBuffer {
196 size: (u16, u16),
197 stride: u16,
198 drawing_second: bool,
199}
200
201impl DoubleBuffer {
202 pub const fn new(resolution: Resolution) -> Self {
204 Self::with_stride(resolution, resolution.height)
205 }
206
207 pub const fn with_stride(resolution: Resolution, stride: u16) -> Self {
216 let height = resolution.height;
217 let stride = if stride < height { height } else { stride };
218 assert!(
219 stride + height <= VRAM_LINES,
220 "two buffers must fit in VRAM's 512 lines"
221 );
222 Self {
223 size: (resolution.width, height),
224 stride,
225 drawing_second: false,
226 }
227 }
228
229 pub const fn size(&self) -> (u16, u16) {
231 self.size
232 }
233
234 pub const fn stride(&self) -> u16 {
236 self.stride
237 }
238
239 pub const fn draw_origin(&self) -> (u16, u16) {
241 (0, self.top(self.drawing_second))
242 }
243
244 pub const fn display_origin(&self) -> (u16, u16) {
246 (0, self.top(!self.drawing_second))
247 }
248
249 const fn top(&self, second: bool) -> u16 {
250 if second {
251 self.stride
252 } else {
253 0
254 }
255 }
256
257 #[doc(alias = "PutDispEnv")]
262 pub fn swap(&mut self, gpu: &mut crate::Gpu) {
263 let shown = self.draw_origin();
264 self.drawing_second = !self.drawing_second;
265 gpu.set_display_start(shown);
266 self.apply_draw_target(gpu);
267 }
268
269 pub fn begin_swap(&mut self, gpu: &mut crate::Gpu) -> u32 {
274 let display = self.begin_deferred_swap();
275 self.apply_draw_target(gpu);
276 display
277 }
278
279 pub fn begin_deferred_swap(&mut self) -> u32 {
289 let (x, y) = self.draw_origin();
290 self.drawing_second = !self.drawing_second;
291 gp1::display_start(x as u32, y as u32)
292 }
293
294 pub fn apply_draw_target(&self, _gpu: &mut crate::Gpu) {
296 let (_, top) = self.draw_origin();
297 let (width, height) = self.size;
298 crate::gpu::write_draw_target(top, width, height);
299 }
300
301 pub fn clear(&self, gpu: &mut crate::Gpu, color: (u8, u8, u8)) {
303 gpu.draw(&crate::prim::FillRect::new(
304 self.draw_origin(),
305 self.size,
306 color,
307 ));
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 const INTERLACE: u32 = 1 << 5;
316 const LINES_480: u32 = 1 << 2;
317
318 #[test]
319 fn a_240_line_mode_is_progressive() {
320 let word = DisplayConfig::new(VideoMode::Ntsc, Resolution::R320X240).mode_command();
321 assert_eq!(word & (INTERLACE | LINES_480), 0);
322 assert_eq!(word & 3, 1, "320-pixel horizontal mode");
323 }
324
325 #[test]
326 fn a_480_line_mode_sets_the_interlace_bit_it_needs() {
327 let word = DisplayConfig::new(VideoMode::Ntsc, Resolution::R640X480).mode_command();
328 assert_eq!(word & (INTERLACE | LINES_480), INTERLACE | LINES_480);
329 assert_eq!(word & 3, 3, "640-pixel horizontal mode");
330 }
331
332 #[test]
333 fn a_480_line_mode_spans_one_field_of_scanlines() {
334 let word =
336 DisplayConfig::new(VideoMode::Ntsc, Resolution::R640X480).vertical_range_command();
337 assert_eq!(word, gp1::v_display_range(0x88 - 120, 0x88 + 120));
338 }
339
340 #[test]
341 fn the_standard_ntsc_picture_starts_at_260h() {
342 let word =
345 DisplayConfig::new(VideoMode::Ntsc, Resolution::R320X240).horizontal_range_command();
346 assert_eq!(word, gp1::h_display_range(0x260, 0x260 + 320 * 8));
347 }
348
349 #[test]
350 fn an_offset_moves_both_windows_and_stops_at_the_blanking_edge() {
351 let display = DisplayConfig::new(VideoMode::Pal, Resolution::R320X256);
352 let moved = display.with_offset((2, 3));
353 assert_eq!(
354 moved.horizontal_range_command(),
355 gp1::h_display_range(0x260 + 16, 0x260 + 16 + 320 * 8)
356 );
357 assert_eq!(
358 moved.vertical_range_command(),
359 gp1::v_display_range(0x23 + 3, 0x23 + 3 + 256)
360 );
361 let clamped = display.with_offset((-200, -100));
362 assert_eq!(
363 clamped.horizontal_range_command(),
364 gp1::h_display_range(0, 320 * 8)
365 );
366 assert_eq!(
367 clamped.vertical_range_command(),
368 gp1::v_display_range(0, 256)
369 );
370 }
371
372 #[test]
373 fn a_deferred_swap_alternates_buffers_without_touching_the_gpu() {
374 let mut buffers = DoubleBuffer::with_stride(Resolution::R320X240, 256);
375 assert_eq!(buffers.begin_deferred_swap(), gp1::display_start(0, 0));
376 assert_eq!(buffers.draw_origin(), (0, 256));
377 assert_eq!(buffers.display_origin(), (0, 0));
378 assert_eq!(buffers.begin_deferred_swap(), gp1::display_start(0, 256));
379 assert_eq!(buffers.draw_origin(), (0, 0));
380 }
381
382 #[test]
383 fn a_short_stride_is_raised_so_buffers_never_overlap() {
384 let buffers = DoubleBuffer::with_stride(Resolution::R320X240, 128);
385 assert_eq!(buffers.stride(), 240);
386 assert_eq!(buffers.size(), (320, 240));
387 }
388
389 #[test]
390 #[should_panic(expected = "two buffers must fit")]
391 fn a_480_line_double_buffer_does_not_fit_vram() {
392 let _ = DoubleBuffer::new(Resolution::R640X480);
393 }
394
395 #[test]
396 fn every_width_spans_the_same_2560_clocks() {
397 for resolution in [
401 Resolution::R256X240,
402 Resolution::R320X240,
403 Resolution::R512X240,
404 Resolution::R640X240,
405 Resolution::R640X480,
406 ] {
407 let word = DisplayConfig::new(VideoMode::Ntsc, resolution).horizontal_range_command();
408 let (x1, x2) = (word & 0xFFF, (word >> 12) & 0xFFF);
409 assert_eq!((x1, x2 - x1), (0x260, 2560), "{resolution:?}");
410 }
411 }
412
413 #[test]
414 fn an_offset_moves_by_whole_pixels_of_the_current_width() {
415 let display = DisplayConfig::new(VideoMode::Ntsc, Resolution::R512X240);
416 let word = display.with_offset((3, 0)).horizontal_range_command();
417 assert_eq!(word & 0xFFF, 0x260 + 3 * 5);
418 }
419}