1#![no_std]
59#![deny(unsafe_op_in_unsafe_fn)]
60#![warn(missing_docs)]
61
62use psx_hw::gpu::{gp0, gp1, pack_xy};
63use psx_io::dma::{self, Channel};
64use psx_io::gpu::{pause_recording, wait_command_ready, write_command, write_display_control};
65use psx_io::periph::GpuDma;
66
67pub const VRAM_WIDTH: u16 = 1024;
69pub const VRAM_HEIGHT: u16 = 512;
71
72#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
90#[repr(transparent)]
91pub struct Color555(u16);
92
93impl Color555 {
94 pub const TRANSPARENT: Self = Self(0);
97 pub const BLACK: Self = Self(0);
99 pub const WHITE: Self = Self(0x7FFF);
101
102 pub const fn rgb8(r: u8, g: u8, b: u8) -> Self {
109 let r = (r as u16) >> 3;
110 let g = (g as u16) >> 3;
111 let b = (b as u16) >> 3;
112 Self(r | (g << 5) | (b << 10))
113 }
114
115 pub const fn rgb5(r: u8, g: u8, b: u8) -> Self {
118 assert!(r < 32, "rgb5: red must be < 32");
119 assert!(g < 32, "rgb5: green must be < 32");
120 assert!(b < 32, "rgb5: blue must be < 32");
121 Self((r as u16) | ((g as u16) << 5) | ((b as u16) << 10))
122 }
123
124 pub const fn raw(value: u16) -> Self {
127 Self(value)
128 }
129
130 pub const fn as_u16(self) -> u16 {
132 self.0
133 }
134
135 pub const fn with_mask_bit(self) -> Self {
142 Self(self.0 | 0x8000)
143 }
144
145 #[doc(alias = "STP")]
158 pub const fn with_semi_transparency(self) -> Self {
159 Self(self.0 | 0x8000)
160 }
161
162 #[doc(alias = "STP")]
164 pub const fn is_semi_transparent(self) -> bool {
165 self.0 & 0x8000 != 0
166 }
167}
168
169#[derive(Copy, Clone, Debug, PartialEq, Eq)]
179pub struct VramRect {
180 pub x: u16,
182 pub y: u16,
184 pub w: u16,
186 pub h: u16,
188}
189
190impl VramRect {
191 pub const fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
193 assert!(w > 0, "VramRect: width must be > 0");
194 assert!(h > 0, "VramRect: height must be > 0");
195 assert!(x <= VRAM_WIDTH, "VramRect: x past VRAM right edge");
196 assert!(y <= VRAM_HEIGHT, "VramRect: y past VRAM bottom edge");
197 assert!(
198 (x as u32) + (w as u32) <= VRAM_WIDTH as u32,
199 "VramRect: (x + w) overflows VRAM width",
200 );
201 assert!(
202 (y as u32) + (h as u32) <= VRAM_HEIGHT as u32,
203 "VramRect: (y + h) overflows VRAM height",
204 );
205 Self { x, y, w, h }
206 }
207
208 pub const fn pixel_count(self) -> u32 {
210 (self.w as u32) * (self.h as u32)
211 }
212
213 pub const fn overlaps(self, other: Self) -> bool {
218 self.x < other.x + other.w
219 && other.x < self.x + self.w
220 && self.y < other.y + other.h
221 && other.y < self.y + self.h
222 }
223}
224
225pub const fn first_vram_overlap(rects: &[VramRect]) -> Option<(usize, usize)> {
254 let mut i = 0;
255 while i < rects.len() {
256 let mut j = i + 1;
257 while j < rects.len() {
258 if rects[i].overlaps(rects[j]) {
259 return Some((i, j));
260 }
261 j += 1;
262 }
263 i += 1;
264 }
265 None
266}
267
268pub const fn vram_layout_is_disjoint(rects: &[VramRect]) -> bool {
271 first_vram_overlap(rects).is_none()
272}
273
274pub const TEXTURE_WINDOW_UNIT_TEXELS: u16 = 8;
283
284pub const TEXTURE_PAGE_TEXELS: u16 = 256;
286
287const TEXTURE_PAGE_UNITS: usize = (TEXTURE_PAGE_TEXELS / TEXTURE_WINDOW_UNIT_TEXELS) as usize;
288
289#[derive(Copy, Clone, Debug, PartialEq, Eq)]
291pub struct TextureWindowPlacement {
292 page_index: u16,
293 origin_u: u8,
294 origin_v: u8,
295 width: u8,
296 height: u8,
297}
298
299impl TextureWindowPlacement {
300 pub const fn page_index(self) -> u16 {
302 self.page_index
303 }
304
305 pub const fn origin_u(self) -> u8 {
307 self.origin_u
308 }
309
310 pub const fn origin_v(self) -> u8 {
312 self.origin_v
313 }
314
315 pub const fn width(self) -> u8 {
317 self.width
318 }
319
320 pub const fn height(self) -> u8 {
322 self.height
323 }
324}
325
326#[derive(Copy, Clone, Debug, PartialEq, Eq)]
333pub struct TextureWindowAtlas<const PAGE_COUNT: usize> {
334 rows: [[u32; TEXTURE_PAGE_UNITS]; PAGE_COUNT],
335}
336
337impl<const PAGE_COUNT: usize> TextureWindowAtlas<PAGE_COUNT> {
338 pub const fn new() -> Self {
340 Self {
341 rows: [[0; TEXTURE_PAGE_UNITS]; PAGE_COUNT],
342 }
343 }
344
345 pub fn clear(&mut self) {
347 for page in &mut self.rows {
348 page.fill(0);
349 }
350 }
351
352 pub fn allocate(&mut self, width: u16, height: u16) -> Option<TextureWindowPlacement> {
358 let width_units = texture_window_units(width)?;
359 let height_units = texture_window_units(height)?;
360 let max_y = TEXTURE_PAGE_UNITS.checked_sub(height_units)?;
361 let max_x = TEXTURE_PAGE_UNITS.checked_sub(width_units)?;
362
363 for page_index in 0..PAGE_COUNT {
364 let mut y = 0usize;
365 while y <= max_y {
366 let mut x = 0usize;
367 while x <= max_x {
368 if texture_region_free(&self.rows[page_index], x, y, width_units, height_units)
369 {
370 texture_region_mark(
371 &mut self.rows[page_index],
372 x,
373 y,
374 width_units,
375 height_units,
376 );
377 return Some(TextureWindowPlacement {
378 page_index: u16::try_from(page_index).ok()?,
379 origin_u: u8::try_from(x * TEXTURE_WINDOW_UNIT_TEXELS as usize).ok()?,
380 origin_v: u8::try_from(y * TEXTURE_WINDOW_UNIT_TEXELS as usize).ok()?,
381 width: u8::try_from(width).ok()?,
382 height: u8::try_from(height).ok()?,
383 });
384 }
385 x += width_units;
386 }
387 y += height_units;
388 }
389 }
390 None
391 }
392
393 pub fn reserve_empty_page(&mut self) -> Option<usize> {
400 for page_index in 0..PAGE_COUNT {
401 if self.rows[page_index].iter().all(|row| *row == 0) {
402 self.rows[page_index].fill(u32::MAX);
403 return Some(page_index);
404 }
405 }
406 None
407 }
408
409 pub fn release(&mut self, placement: TextureWindowPlacement) {
414 let page = placement.page_index() as usize;
415 let (Some(width_units), Some(height_units)) = (
416 texture_window_units(placement.width() as u16),
417 texture_window_units(placement.height() as u16),
418 ) else {
419 return;
420 };
421 let x = (placement.origin_u() / TEXTURE_WINDOW_UNIT_TEXELS as u8) as usize;
422 let y = (placement.origin_v() / TEXTURE_WINDOW_UNIT_TEXELS as u8) as usize;
423 if page >= PAGE_COUNT {
424 return;
425 }
426 let mask = texture_region_mask(x, width_units);
427 for row in &mut self.rows[page][y..y + height_units] {
428 *row &= !mask;
429 }
430 }
431
432 pub fn release_page(&mut self, page_index: usize) {
435 if page_index < PAGE_COUNT {
436 self.rows[page_index].fill(0);
437 }
438 }
439
440 pub fn is_page_empty(&self, page_index: usize) -> bool {
444 self.rows
445 .get(page_index)
446 .is_some_and(|rows| rows.iter().all(|row| *row == 0))
447 }
448}
449
450impl<const PAGE_COUNT: usize> Default for TextureWindowAtlas<PAGE_COUNT> {
451 fn default() -> Self {
452 Self::new()
453 }
454}
455
456fn texture_window_units(size: u16) -> Option<usize> {
457 if !(TEXTURE_WINDOW_UNIT_TEXELS..=128).contains(&size)
458 || !size.is_power_of_two()
459 || !size.is_multiple_of(TEXTURE_WINDOW_UNIT_TEXELS)
460 {
461 return None;
462 }
463 Some((size / TEXTURE_WINDOW_UNIT_TEXELS) as usize)
464}
465
466fn texture_region_mask(x: usize, width: usize) -> u32 {
467 ((1u32 << width) - 1) << x
468}
469
470fn texture_region_free(
471 rows: &[u32; TEXTURE_PAGE_UNITS],
472 x: usize,
473 y: usize,
474 width: usize,
475 height: usize,
476) -> bool {
477 let mask = texture_region_mask(x, width);
478 for row in &rows[y..y + height] {
479 if row & mask != 0 {
480 return false;
481 }
482 }
483 true
484}
485
486fn texture_region_mark(
487 rows: &mut [u32; TEXTURE_PAGE_UNITS],
488 x: usize,
489 y: usize,
490 width: usize,
491 height: usize,
492) {
493 let mask = texture_region_mask(x, width);
494 for row in &mut rows[y..y + height] {
495 *row |= mask;
496 }
497}
498
499#[derive(Copy, Clone, Debug, PartialEq, Eq)]
506#[repr(u8)]
507pub enum TextureDepth {
508 Bit4 = 0,
510 Bit8 = 1,
512 Bit15 = 2,
514}
515
516impl TextureDepth {
517 const fn as_u16(self) -> u16 {
518 self as u16
519 }
520
521 pub const fn texels_per_halfword(self) -> u16 {
524 match self {
525 TextureDepth::Bit4 => 4,
526 TextureDepth::Bit8 => 2,
527 TextureDepth::Bit15 => 1,
528 }
529 }
530}
531
532#[derive(Copy, Clone, Debug, PartialEq, Eq)]
548pub struct TexturePage {
549 x: u16,
550 y: u16,
551 depth: TextureDepth,
552}
553
554impl TexturePage {
555 #[allow(clippy::manual_is_multiple_of)]
558 pub const fn new(x: u16, y: u16, depth: TextureDepth) -> Self {
559 assert!(x % 64 == 0, "TexturePage: x must be a multiple of 64");
560 assert!(x < VRAM_WIDTH, "TexturePage: x past VRAM");
561 assert!(y == 0 || y == 256, "TexturePage: y must be 0 or 256");
562 Self { x, y, depth }
563 }
564
565 pub const fn x(self) -> u16 {
567 self.x
568 }
569 pub const fn y(self) -> u16 {
571 self.y
572 }
573 pub const fn depth(self) -> TextureDepth {
575 self.depth
576 }
577
578 pub const fn covering_rect(self) -> VramRect {
581 let w = match self.depth {
582 TextureDepth::Bit4 => 64,
583 TextureDepth::Bit8 => 128,
584 TextureDepth::Bit15 => 256,
585 };
586 VramRect::new(self.x, self.y, w, 256)
590 }
591
592 #[doc(alias = "getTPage")]
607 pub const fn uv_word(self, semi_trans: u8) -> u16 {
608 assert!(semi_trans < 4, "semi_trans must be 0..4");
609 let tpx = (self.x / 64) & 0xF;
610 let tpy = if self.y == 256 { 1 } else { 0 };
611 let depth = self.depth.as_u16();
612 tpx | (tpy << 4) | ((semi_trans as u16) << 5) | (depth << 7)
613 }
614
615 pub fn apply_as_draw_mode(self) {
619 wait_command_ready();
620 write_command(gp0::draw_mode(
621 (self.x / 64) as u32,
622 if self.y == 256 { 1 } else { 0 },
623 0,
624 self.depth as u32,
625 false,
626 true,
627 ));
628 wait_command_ready();
632 write_command(gp0::tex_window(0, 0, 0, 0));
633 }
634}
635
636#[derive(Copy, Clone, Debug, PartialEq, Eq)]
640pub struct Clut {
641 x: u16,
642 y: u16,
643}
644
645impl Clut {
646 #[allow(clippy::manual_is_multiple_of)]
648 pub const fn new(x: u16, y: u16) -> Self {
649 assert!(x % 16 == 0, "Clut: x must be a multiple of 16");
650 assert!(x < VRAM_WIDTH, "Clut: x past VRAM");
651 assert!(y < VRAM_HEIGHT, "Clut: y past VRAM");
652 Self { x, y }
653 }
654
655 pub const fn x(self) -> u16 {
657 self.x
658 }
659 pub const fn y(self) -> u16 {
661 self.y
662 }
663
664 #[doc(alias = "getClut")]
672 pub const fn uv_word(self) -> u16 {
673 let cx = (self.x / 16) & 0x3F;
674 let cy = self.y & 0x1FF;
675 cx | (cy << 6)
676 }
677}
678
679const ALLOC_COL_W: u16 = 64;
685const ALLOC_ROW_H: u16 = 16;
687pub const VRAM_ALLOC_COLS: usize = (VRAM_WIDTH / ALLOC_COL_W) as usize;
689pub const VRAM_ALLOC_ROWS: usize = (VRAM_HEIGHT / ALLOC_ROW_H) as usize;
691const ALLOC_PAGE_ROWS: usize = (TEXTURE_PAGE_TEXELS / ALLOC_ROW_H) as usize;
693const CLUT_SLOTS_PER_ROW: u16 = VRAM_WIDTH / 16;
695
696#[derive(Copy, Clone, Debug, PartialEq, Eq)]
702pub enum VramHandle {
703 Empty,
705 Rect(VramRect),
707 Window(TextureWindowPlacement),
709 RoomPage(u16),
711 Clut {
713 x: u16,
715 y: u16,
717 entries: u16,
719 },
720}
721
722pub trait VramRegionSource {
725 fn alloc_page_run(
728 &mut self,
729 count: u16,
730 depth: TextureDepth,
731 page_y: u16,
732 ) -> Option<(TexturePage, VramHandle)>;
733
734 fn alloc_clut(&mut self, entries: u16) -> Option<(Clut, VramHandle)>;
736}
737
738#[derive(Copy, Clone, Debug)]
744pub struct ClutRowAllocator<const ROWS: usize> {
745 base_y: u16,
746 rows: [u64; ROWS],
748}
749
750impl<const ROWS: usize> ClutRowAllocator<ROWS> {
751 pub const fn new(base_y: u16) -> Self {
753 Self {
754 base_y,
755 rows: [0; ROWS],
756 }
757 }
758
759 fn slot_count(entries: u16) -> usize {
760 (entries.max(1) as usize).div_ceil(16)
761 }
762
763 pub fn alloc(&mut self, entries: u16) -> Option<Clut> {
765 let slots = Self::slot_count(entries);
766 let cap = CLUT_SLOTS_PER_ROW as usize;
767 if slots == 0 || slots > cap.min(64) {
768 return None;
769 }
770 let want: u64 = if slots == 64 {
772 u64::MAX
774 } else {
775 (1u64 << slots) - 1
776 };
777 for (r, row) in self.rows.iter_mut().enumerate() {
778 let mut s = 0usize;
779 while s + slots <= cap.min(64) {
780 let mask = want << s;
781 if *row & mask == 0 {
782 *row |= mask;
783 return Some(Clut::new((s as u16) * 16, self.base_y + r as u16));
784 }
785 s += 1;
786 }
787 }
788 None
789 }
790
791 pub fn free(&mut self, clut: Clut, entries: u16) {
793 let slots = Self::slot_count(entries);
794 let r = clut.y().saturating_sub(self.base_y) as usize;
795 let s = (clut.x() / 16) as usize;
796 if r >= ROWS || s + slots > 64 {
797 return;
798 }
799 let mask: u64 = if slots == 64 {
801 u64::MAX
803 } else {
804 (1u64 << slots) - 1
805 } << s;
806 self.rows[r] &= !mask;
807 }
808}
809
810#[derive(Copy, Clone, Debug)]
816pub struct VramAllocator<const ROOM_PAGES: usize, const CLUT_ROWS: usize> {
817 grid: [u16; VRAM_ALLOC_ROWS],
818 clut: ClutRowAllocator<CLUT_ROWS>,
819 room: TextureWindowAtlas<ROOM_PAGES>,
820 room_pages: [Option<TexturePage>; ROOM_PAGES],
824 room_page_handles: [VramHandle; ROOM_PAGES],
825 room_base_x: u16,
826 room_base_y: u16,
827}
828
829impl<const ROOM_PAGES: usize, const CLUT_ROWS: usize> VramAllocator<ROOM_PAGES, CLUT_ROWS> {
830 pub const fn new(clut_base_y: u16) -> Self {
832 Self {
833 grid: [0; VRAM_ALLOC_ROWS],
834 clut: ClutRowAllocator::new(clut_base_y),
835 room: TextureWindowAtlas::new(),
836 room_pages: [None; ROOM_PAGES],
837 room_page_handles: [VramHandle::Empty; ROOM_PAGES],
838 room_base_x: 0,
839 room_base_y: 0,
840 }
841 }
842
843 fn span(x: u16, y: u16, w: u16, h: u16) -> (usize, usize, usize, usize) {
844 let c0 = (x / ALLOC_COL_W) as usize;
845 let c1 = ((x as usize + w as usize).div_ceil(ALLOC_COL_W as usize)).min(VRAM_ALLOC_COLS);
846 let r0 = (y / ALLOC_ROW_H) as usize;
847 let r1 = ((y as usize + h as usize).div_ceil(ALLOC_ROW_H as usize)).min(VRAM_ALLOC_ROWS);
848 (c0, c1, r0, r1)
849 }
850
851 #[allow(dead_code)] fn rect_free(&self, x: u16, y: u16, w: u16, h: u16) -> bool {
853 let (c0, c1, r0, r1) = Self::span(x, y, w, h);
854 for r in r0..r1 {
855 for c in c0..c1 {
856 if self.grid[r] & (1 << c) != 0 {
857 return false;
858 }
859 }
860 }
861 true
862 }
863
864 fn set_rect(&mut self, x: u16, y: u16, w: u16, h: u16, occupied: bool) {
865 let (c0, c1, r0, r1) = Self::span(x, y, w, h);
866 for r in r0..r1 {
867 for c in c0..c1 {
868 if occupied {
869 self.grid[r] |= 1 << c;
870 } else {
871 self.grid[r] &= !(1 << c);
872 }
873 }
874 }
875 }
876
877 pub fn reserve_rect(&mut self, rect: VramRect) -> VramHandle {
881 self.set_rect(rect.x, rect.y, rect.w, rect.h, true);
882 VramHandle::Rect(rect)
883 }
884
885 pub fn reserve_room_band(&mut self, base_x: u16, base_y: u16) {
889 self.room_base_x = base_x;
890 self.room_base_y = base_y;
891 }
892
893 fn ensure_room_page(&mut self, page_index: usize) -> Option<TexturePage> {
894 if let Some(tpage) = self.room_pages.get(page_index).copied().flatten() {
895 return Some(tpage);
896 }
897 if page_index >= ROOM_PAGES {
898 return None;
899 }
900 let preferred_x = self
901 .room_base_x
902 .checked_add((page_index as u16).checked_mul(ALLOC_COL_W)?)?;
903 let x = if preferred_x + ALLOC_COL_W <= VRAM_WIDTH
904 && self.rect_free(
905 preferred_x,
906 self.room_base_y,
907 ALLOC_COL_W,
908 TEXTURE_PAGE_TEXELS,
909 ) {
910 preferred_x
911 } else {
912 self.find_page_run(1, self.room_base_y)?
913 };
914 self.set_rect(x, self.room_base_y, ALLOC_COL_W, TEXTURE_PAGE_TEXELS, true);
915 let tpage = TexturePage::new(x, self.room_base_y, TextureDepth::Bit4);
916 self.room_pages[page_index] = Some(tpage);
917 self.room_page_handles[page_index] = VramHandle::Rect(VramRect::new(
918 x,
919 self.room_base_y,
920 ALLOC_COL_W,
921 TEXTURE_PAGE_TEXELS,
922 ));
923 Some(tpage)
924 }
925
926 fn release_room_page_if_empty(&mut self, page_index: usize) {
927 if page_index >= ROOM_PAGES || !self.room.is_page_empty(page_index) {
928 return;
929 }
930 let handle = core::mem::replace(&mut self.room_page_handles[page_index], VramHandle::Empty);
931 if let VramHandle::Rect(rect) = handle {
932 self.set_rect(rect.x, rect.y, rect.w, rect.h, false);
933 }
934 self.room_pages[page_index] = None;
935 }
936
937 fn find_page_run(&self, count: u16, page_y: u16) -> Option<u16> {
938 let r0 = (page_y / ALLOC_ROW_H) as usize;
939 let r1 = r0 + ALLOC_PAGE_ROWS;
940 if r1 > VRAM_ALLOC_ROWS || count == 0 {
941 return None;
942 }
943 let n = count as usize;
944 let mut c = 0usize;
945 while c + n <= VRAM_ALLOC_COLS {
946 let cols_free = (c..c + n).all(|col| (r0..r1).all(|r| self.grid[r] & (1 << col) == 0));
947 if cols_free {
948 return Some((c as u16) * ALLOC_COL_W);
949 }
950 c += 1;
951 }
952 None
953 }
954
955 pub fn alloc_window(
958 &mut self,
959 width_texels: u16,
960 height_texels: u16,
961 ) -> Option<(TexturePage, TextureWindowPlacement, VramHandle)> {
962 let placement = self.room.allocate(width_texels, height_texels)?;
963 let page_index = placement.page_index() as usize;
964 let Some(tpage) = self.ensure_room_page(page_index) else {
965 self.room.release(placement);
966 return None;
967 };
968 Some((tpage, placement, VramHandle::Window(placement)))
969 }
970
971 pub fn alloc_room_page(&mut self) -> Option<(TexturePage, VramHandle)> {
975 let page_index = self.room.reserve_empty_page()?;
976 let Some(tpage) = self.ensure_room_page(page_index) else {
977 self.room.release_page(page_index);
978 return None;
979 };
980 Some((tpage, VramHandle::RoomPage(page_index as u16)))
981 }
982
983 pub fn alloc_model_slot(
990 &mut self,
991 halfwords_per_row: u16,
992 depth: TextureDepth,
993 ) -> Option<(TexturePage, VramHandle)> {
994 if !matches!(depth, TextureDepth::Bit4 | TextureDepth::Bit8) {
995 return None;
996 }
997 let pages = halfwords_per_row.div_ceil(ALLOC_COL_W).max(1);
998 let x = self.find_page_run(pages, 256)?;
999 self.set_rect(x, 256, pages * ALLOC_COL_W, TEXTURE_PAGE_TEXELS, true);
1000 Some((
1001 TexturePage::new(x, 256, depth),
1002 VramHandle::Rect(VramRect::new(
1003 x,
1004 256,
1005 pages * ALLOC_COL_W,
1006 TEXTURE_PAGE_TEXELS,
1007 )),
1008 ))
1009 }
1010
1011 pub fn free(&mut self, handle: VramHandle) {
1013 match handle {
1014 VramHandle::Empty => {}
1015 VramHandle::Rect(rect) => self.set_rect(rect.x, rect.y, rect.w, rect.h, false),
1016 VramHandle::Window(placement) => {
1017 let page = placement.page_index() as usize;
1018 self.room.release(placement);
1019 self.release_room_page_if_empty(page);
1020 }
1021 VramHandle::RoomPage(page) => {
1022 let page = page as usize;
1023 self.room.release_page(page);
1024 self.release_room_page_if_empty(page);
1025 }
1026 VramHandle::Clut { x, y, entries } => self.clut.free(Clut::new(x, y), entries),
1027 }
1028 }
1029}
1030
1031impl<const ROOM_PAGES: usize, const CLUT_ROWS: usize> VramRegionSource
1032 for VramAllocator<ROOM_PAGES, CLUT_ROWS>
1033{
1034 fn alloc_page_run(
1035 &mut self,
1036 count: u16,
1037 depth: TextureDepth,
1038 page_y: u16,
1039 ) -> Option<(TexturePage, VramHandle)> {
1040 let x = self.find_page_run(count, page_y)?;
1041 self.set_rect(x, page_y, count * ALLOC_COL_W, TEXTURE_PAGE_TEXELS, true);
1042 Some((
1043 TexturePage::new(x, page_y, depth),
1044 VramHandle::Rect(VramRect::new(
1045 x,
1046 page_y,
1047 count * ALLOC_COL_W,
1048 TEXTURE_PAGE_TEXELS,
1049 )),
1050 ))
1051 }
1052
1053 fn alloc_clut(&mut self, entries: u16) -> Option<(Clut, VramHandle)> {
1054 let clut = self.clut.alloc(entries)?;
1055 Some((
1056 clut,
1057 VramHandle::Clut {
1058 x: clut.x(),
1059 y: clut.y(),
1060 entries,
1061 },
1062 ))
1063 }
1064}
1065
1066pub fn upload_16bpp(rect: VramRect, pixels: &[u16]) {
1080 let expected = rect.pixel_count();
1081 assert!(
1084 pixels.len() as u32 == expected,
1085 "upload_16bpp: pixels.len() != rect.w * rect.h"
1086 );
1087 assert!(
1088 expected.is_multiple_of(2),
1089 "upload_16bpp: odd pixel count not supported; round the rect up"
1090 );
1091
1092 let _pause = pause_recording();
1104 copy_to_vram_header(rect);
1105 let mut i = 0;
1106 while i + 1 < pixels.len() {
1107 let lo = pixels[i] as u32;
1108 let hi = pixels[i + 1] as u32;
1109 psx_io::gpu::write_command_unguarded(lo | (hi << 16));
1110 i += 2;
1111 }
1112}
1113
1114pub fn upload_bytes(rect: VramRect, bytes: &[u8]) {
1124 let expected = (rect.pixel_count() as usize) * 2;
1125 assert_eq!(
1126 bytes.len(),
1127 expected,
1128 "upload_bytes: bytes.len() ({}) != 2 × rect.w × rect.h ({})",
1129 bytes.len(),
1130 expected,
1131 );
1132 assert!(
1133 bytes.len() >= 2 && bytes.len().is_multiple_of(2),
1134 "upload_bytes: byte count must be a positive multiple of 2"
1135 );
1136
1137 let _pause = pause_recording();
1144 copy_to_vram_header(rect);
1145 let mut i = 0;
1146 while i < bytes.len() {
1147 psx_io::gpu::write_command_unguarded(packed_upload_word(bytes, i));
1148 i += 4;
1149 }
1150}
1151
1152#[inline]
1153fn packed_upload_word(bytes: &[u8], offset: usize) -> u32 {
1154 u32::from_le_bytes([
1155 bytes[offset],
1156 bytes[offset + 1],
1157 bytes.get(offset + 2).copied().unwrap_or(0),
1158 bytes.get(offset + 3).copied().unwrap_or(0),
1159 ])
1160}
1161
1162pub fn upload_bytes_aligned(rect: VramRect, bytes: &[u8]) {
1165 if cfg!(target_endian = "little")
1166 && bytes.as_ptr().align_offset(4) == 0
1167 && bytes.len().is_multiple_of(4)
1168 {
1169 let words =
1170 unsafe { core::slice::from_raw_parts(bytes.as_ptr().cast::<u32>(), bytes.len() / 4) };
1173 upload_words(rect, words);
1174 } else {
1175 upload_bytes(rect, bytes);
1176 }
1177}
1178
1179#[doc(alias = "LoadImage")]
1187pub fn upload_words(rect: VramRect, words: &[u32]) {
1188 let expected_words = rect.pixel_count() as usize / 2;
1189 assert!(
1190 rect.pixel_count().is_multiple_of(2),
1191 "upload_words: odd pixel count is not supported"
1192 );
1193 assert_eq!(
1194 words.len(),
1195 expected_words,
1196 "upload_words: words.len() ({}) != rect.w*rect.h/2 ({})",
1197 words.len(),
1198 expected_words,
1199 );
1200 assert!(!words.is_empty(), "upload_words: empty upload");
1201
1202 let _pause = pause_recording();
1203 copy_to_vram_header(rect);
1204 for &word in words {
1205 psx_io::gpu::write_command_unguarded(word);
1206 }
1207}
1208
1209#[inline]
1213fn copy_to_vram_header(rect: VramRect) {
1214 if !dma::wait_done(Channel::Gpu, dma::DEFAULT_SPINS) {
1221 dma::abort(Channel::Gpu);
1222 }
1223 wait_command_ready();
1224 write_command(gp0::COPY_CPU_TO_VRAM);
1225 write_command(pack_xy(rect.x, rect.y));
1226 write_command(pack_xy(rect.w, rect.h));
1227}
1228
1229#[doc(alias = "LoadImage")]
1244pub fn dma_copy_to_vram(_dma: &mut GpuDma, rect: VramRect, words: &[u32]) -> bool {
1245 if words.len() < copy_words(rect) {
1246 return false;
1247 }
1248 if !unsafe { kick_copy_to_vram(rect, words.as_ptr()) } {
1252 return false;
1253 }
1254 dma::wait_or_abort(Channel::Gpu, dma::DEFAULT_SPINS)
1257}
1258
1259#[doc(alias = "LoadImage")]
1276pub unsafe fn dma_copy_to_vram_start(_dma: &mut GpuDma, rect: VramRect, words: *const u32) -> bool {
1277 unsafe { kick_copy_to_vram(rect, words) }
1279}
1280
1281#[inline]
1283pub fn dma_copy_to_vram_done() -> bool {
1284 !dma::is_busy(Channel::Gpu)
1285}
1286
1287fn copy_words(rect: VramRect) -> usize {
1289 (rect.w / 2) as usize * rect.h as usize
1290}
1291
1292unsafe fn kick_copy_to_vram(rect: VramRect, src: *const u32) -> bool {
1301 if !rect.w.is_multiple_of(2) || rect.w == 0 || rect.h == 0 {
1302 return false;
1303 }
1304 let words_per_row = rect.w / 2;
1305 if words_per_row > 16 {
1310 return false;
1311 }
1312 let _pause = pause_recording();
1313 copy_to_vram_header(rect);
1314 write_display_control(gp1::dma_direction(2));
1317 dma::enable_channel(Channel::Gpu);
1318 unsafe {
1322 dma::start(
1323 Channel::Gpu,
1324 dma::Transfer {
1325 address: src as u32,
1326 size: dma::size_blocks(words_per_row, rect.h),
1327 control: psx_hw::dma::CHCR_TO_DEVICE
1328 | psx_hw::dma::CHCR_SYNC_BLOCK
1329 | psx_hw::dma::CHCR_START,
1330 },
1331 )
1332 };
1333 true
1334}
1335
1336pub fn upload_15bpp(rect: VramRect, pixels: &[Color555]) {
1339 let as_u16: &[u16] = unsafe {
1340 core::slice::from_raw_parts(pixels.as_ptr() as *const u16, pixels.len())
1343 };
1344 upload_16bpp(rect, as_u16);
1345}
1346
1347pub fn upload_clut(clut: Clut, entries: &[Color555]) {
1351 let rect = VramRect::new(clut.x(), clut.y(), entries.len() as u16, 1);
1352 upload_15bpp(rect, entries);
1353}
1354
1355#[deprecated(note = "renamed to `TexturePage`")]
1361pub type Tpage = TexturePage;
1362
1363#[deprecated(note = "renamed to `TextureDepth`")]
1365pub type TexDepth = TextureDepth;
1366
1367impl TexturePage {
1368 #[deprecated(note = "renamed to `uv_word`")]
1370 #[inline(always)]
1371 pub const fn uv_tpage_word(self, semi_trans: u8) -> u16 {
1372 self.uv_word(semi_trans)
1373 }
1374}
1375
1376impl Clut {
1377 #[deprecated(note = "renamed to `uv_word`")]
1379 #[inline(always)]
1380 pub const fn uv_clut_word(self) -> u16 {
1381 self.uv_word()
1382 }
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387 use super::*;
1388
1389 #[test]
1390 fn dma_copy_to_vram_refuses_what_it_cannot_send_whole() {
1391 let mut dma = unsafe { GpuDma::steal() };
1394 let words = [0u32; 8];
1395 assert!(!dma_copy_to_vram(
1397 &mut dma,
1398 VramRect::new(0, 0, 8, 2),
1399 &words[..7]
1400 ));
1401 assert!(!dma_copy_to_vram(
1403 &mut dma,
1404 VramRect::new(0, 0, 3, 1),
1405 &words
1406 ));
1407 assert!(!dma_copy_to_vram(
1408 &mut dma,
1409 VramRect {
1410 x: 0,
1411 y: 0,
1412 w: 0,
1413 h: 1
1414 },
1415 &words
1416 ));
1417 assert!(!dma_copy_to_vram(
1418 &mut dma,
1419 VramRect::new(0, 0, 34, 1),
1420 &[0; 17]
1421 ));
1422 assert!(!dma_copy_to_vram(
1423 &mut dma,
1424 VramRect {
1425 x: 0,
1426 y: 0,
1427 w: 2,
1428 h: 0
1429 },
1430 &words
1431 ));
1432 }
1433
1434 #[test]
1435 fn byte_upload_word_zero_pads_an_odd_final_halfword() {
1436 assert_eq!(
1437 packed_upload_word(&[0x11, 0x22, 0x33, 0x44], 0),
1438 0x4433_2211
1439 );
1440 assert_eq!(packed_upload_word(&[0x55, 0x66], 0), 0x0000_6655);
1441 }
1442
1443 #[test]
1444 fn stp_is_the_mask_bit_named_for_its_palette_meaning() {
1445 let opaque = Color555::rgb8(255, 255, 255);
1446 assert!(!opaque.is_semi_transparent());
1447 assert_eq!(
1448 opaque.with_semi_transparency().as_u16(),
1449 opaque.with_mask_bit().as_u16()
1450 );
1451 assert!(opaque.with_semi_transparency().is_semi_transparent());
1452 assert_eq!(Color555::TRANSPARENT.as_u16(), 0);
1455 assert!(!Color555::TRANSPARENT.is_semi_transparent());
1456 }
1457
1458 #[test]
1459 fn color_rgb8_truncates_low_bits() {
1460 assert_eq!(Color555::rgb8(0, 0, 0).as_u16(), 0);
1461 assert_eq!(Color555::rgb8(255, 255, 255).as_u16(), 0x7FFF);
1462 assert_eq!(Color555::rgb8(24, 0, 0).as_u16(), 3);
1464 }
1465
1466 #[test]
1467 fn color_rgb5_rejects_out_of_range_via_panic() {
1468 assert_eq!(Color555::rgb5(31, 0, 0).as_u16(), 31);
1471 assert_eq!(Color555::rgb5(0, 31, 0).as_u16(), 31 << 5);
1472 assert_eq!(Color555::rgb5(0, 0, 31).as_u16(), 31 << 10);
1473 }
1474
1475 #[test]
1476 #[should_panic = "red must be < 32"]
1477 fn color_rgb5_panics_on_32() {
1478 let _ = Color555::rgb5(32, 0, 0);
1479 }
1480
1481 #[test]
1482 fn vram_rect_new_accepts_valid_positions() {
1483 let _ = VramRect::new(0, 0, 64, 64);
1484 let _ = VramRect::new(VRAM_WIDTH - 1, 0, 1, 1);
1485 let _ = VramRect::new(0, VRAM_HEIGHT - 1, 1, 1);
1486 }
1487
1488 #[test]
1489 #[should_panic = "overflows VRAM width"]
1490 fn vram_rect_new_rejects_horizontal_overflow() {
1491 let _ = VramRect::new(1000, 0, 100, 10);
1492 }
1493
1494 #[test]
1495 fn texture_window_atlas_packs_32x32_textures_inside_one_page() {
1496 let mut atlas = TextureWindowAtlas::<1>::new();
1497
1498 let first = atlas.allocate(32, 32).expect("first texture fits");
1499 let second = atlas.allocate(32, 32).expect("second texture fits");
1500 let eighth = {
1501 let mut placement = second;
1502 for _ in 2..8 {
1503 placement = atlas.allocate(32, 32).expect("top row texture fits");
1504 }
1505 placement
1506 };
1507 let ninth = atlas.allocate(32, 32).expect("second row texture fits");
1508
1509 assert_eq!(first.page_index(), 0);
1510 assert_eq!((first.origin_u(), first.origin_v()), (0, 0));
1511 assert_eq!((second.origin_u(), second.origin_v()), (32, 0));
1512 assert_eq!((eighth.origin_u(), eighth.origin_v()), (224, 0));
1513 assert_eq!((ninth.origin_u(), ninth.origin_v()), (0, 32));
1514 }
1515
1516 #[test]
1517 fn texture_window_atlas_moves_to_next_page_when_current_page_is_full() {
1518 let mut atlas = TextureWindowAtlas::<2>::new();
1519
1520 for _ in 0..16 {
1521 atlas
1522 .allocate(64, 64)
1523 .expect("64x64 slot fits in first page");
1524 }
1525 let next = atlas.allocate(64, 64).expect("second page has space");
1526
1527 assert_eq!(next.page_index(), 1);
1528 assert_eq!((next.origin_u(), next.origin_v()), (0, 0));
1529 }
1530
1531 #[test]
1532 fn texture_window_atlas_reserves_empty_page_for_unwindowed_texture() {
1533 let mut atlas = TextureWindowAtlas::<2>::new();
1534
1535 let page = atlas.reserve_empty_page().expect("page reserved");
1536 assert_eq!(page, 0);
1537 let next = atlas.allocate(64, 64).expect("second page still free");
1538
1539 assert_eq!(next.page_index(), 1);
1540 assert_eq!((next.origin_u(), next.origin_v()), (0, 0));
1541 }
1542
1543 #[test]
1544 fn texture_window_atlas_aligns_each_allocation_to_its_own_size() {
1545 let mut atlas = TextureWindowAtlas::<1>::new();
1546
1547 atlas.allocate(32, 32).expect("small texture fits");
1548 let large = atlas.allocate(64, 64).expect("large texture fits");
1549
1550 assert_eq!((large.origin_u(), large.origin_v()), (64, 0));
1551 }
1552
1553 #[test]
1554 fn texture_window_atlas_rejects_non_texture_window_dimensions() {
1555 let mut atlas = TextureWindowAtlas::<1>::new();
1556
1557 assert!(atlas.allocate(48, 32).is_none());
1558 assert!(atlas.allocate(4, 32).is_none());
1559 assert!(atlas.allocate(256, 32).is_none());
1560 }
1561
1562 #[test]
1563 fn tpage_encodes_uv_word_correctly() {
1564 let tp = TexturePage::new(640, 0, TextureDepth::Bit15);
1565 let word = tp.uv_word(0);
1567 assert_eq!(word & 0xF, 10, "tpx");
1568 assert_eq!((word >> 4) & 1, 0, "tpy");
1569 assert_eq!((word >> 5) & 3, 0, "semi_trans");
1570 assert_eq!((word >> 7) & 3, 2, "depth");
1571 }
1572
1573 #[test]
1574 #[should_panic = "x must be a multiple of 64"]
1575 fn tpage_rejects_misaligned_x() {
1576 let _ = TexturePage::new(32, 0, TextureDepth::Bit8);
1577 }
1578
1579 #[test]
1580 #[should_panic = "y must be 0 or 256"]
1581 fn tpage_rejects_bad_y() {
1582 let _ = TexturePage::new(0, 128, TextureDepth::Bit4);
1583 }
1584
1585 #[test]
1586 fn clut_encodes_uv_word_correctly() {
1587 let cl = Clut::new(640, 240);
1588 let word = cl.uv_word();
1590 assert_eq!(word & 0x3F, 40, "cx");
1591 assert_eq!((word >> 6) & 0x1FF, 240, "cy");
1592 }
1593
1594 #[test]
1595 #[should_panic = "x must be a multiple of 16"]
1596 fn clut_rejects_misaligned_x() {
1597 let _ = Clut::new(8, 0);
1598 }
1599
1600 #[test]
1601 fn tex_depth_texels_per_halfword() {
1602 assert_eq!(TextureDepth::Bit4.texels_per_halfword(), 4);
1603 assert_eq!(TextureDepth::Bit8.texels_per_halfword(), 2);
1604 assert_eq!(TextureDepth::Bit15.texels_per_halfword(), 1);
1605 }
1606
1607 #[test]
1608 fn vram_alloc_reserves_framebuffer_then_packs_pages_past_it() {
1609 let mut a = VramAllocator::<6, 16>::new(480);
1610 a.reserve_rect(VramRect::new(0, 0, 320, 480));
1611 let (tp, _h) = a
1612 .alloc_page_run(3, TextureDepth::Bit4, 0)
1613 .expect("3 contiguous pages");
1614 assert_eq!(tp.x() % 64, 0, "page X is 64-aligned");
1615 assert!(tp.x() >= 320, "page must not overlap the framebuffer");
1616 assert_eq!(tp.y(), 0);
1617 }
1618
1619 #[test]
1620 fn vram_alloc_free_round_trips_a_page() {
1621 let mut a = VramAllocator::<6, 16>::new(480);
1622 let (tp1, h1) = a.alloc_page_run(1, TextureDepth::Bit4, 0).unwrap();
1623 let (tp2, _h2) = a.alloc_page_run(1, TextureDepth::Bit4, 0).unwrap();
1624 assert_ne!(tp1.x(), tp2.x());
1625 a.free(h1);
1626 let (tp3, _h3) = a.alloc_page_run(1, TextureDepth::Bit4, 0).unwrap();
1627 assert_eq!(tp3.x(), tp1.x(), "freed page is reused");
1628 }
1629
1630 #[test]
1631 fn vram_alloc_window_lands_in_reserved_room_band() {
1632 let mut a = VramAllocator::<2, 16>::new(480);
1633 a.reserve_room_band(640, 0);
1634 let (tp, pl, _h) = a.alloc_window(32, 32).expect("window fits");
1635 assert_eq!(tp.x(), 640, "first window is at room band base");
1636 assert_eq!((pl.origin_u(), pl.origin_v()), (0, 0));
1637 }
1638
1639 #[test]
1640 fn lazy_room_page_returns_physical_backing_after_last_window() {
1641 let mut a = VramAllocator::<6, 16>::new(480);
1642 a.reserve_rect(VramRect::new(0, 0, 320, 480));
1643 a.reserve_room_band(640, 0);
1644 let (_, _, window) = a.alloc_window(64, 64).expect("room window");
1645 assert!(
1646 a.alloc_page_run(6, TextureDepth::Bit4, 0).is_none(),
1647 "live preferred room page interrupts the six-page top run"
1648 );
1649 a.free(window);
1650 assert!(
1651 a.alloc_page_run(6, TextureDepth::Bit4, 0).is_some(),
1652 "last-window release must return the physical room page"
1653 );
1654 }
1655
1656 #[test]
1657 fn model_slots_preserve_indexed_texture_depth() {
1658 let mut a = VramAllocator::<2, 16>::new(480);
1659 let (bit4, _h4) = a.alloc_model_slot(32, TextureDepth::Bit4).unwrap();
1660 let (bit8, _h8) = a.alloc_model_slot(64, TextureDepth::Bit8).unwrap();
1661
1662 assert_eq!(bit4.depth(), TextureDepth::Bit4);
1663 assert_eq!(bit8.depth(), TextureDepth::Bit8);
1664 assert_ne!(bit4.x(), bit8.x());
1665 assert!(a.alloc_model_slot(64, TextureDepth::Bit15).is_none());
1666 }
1667
1668 #[test]
1669 fn clut_row_allocator_packs_and_reuses_slots() {
1670 let mut c = ClutRowAllocator::<4>::new(480);
1671 let a = c.alloc(16).unwrap();
1672 let b = c.alloc(16).unwrap();
1673 assert_eq!((a.x(), a.y()), (0, 480));
1674 assert_eq!((b.x(), b.y()), (16, 480));
1675 let big = c.alloc(256).unwrap(); assert_eq!((big.x(), big.y()), (32, 480));
1677 c.free(a, 16);
1678 let reused = c.alloc(16).unwrap();
1679 assert_eq!((reused.x(), reused.y()), (0, 480), "freed slot reused");
1680 }
1681
1682 #[test]
1683 fn texture_window_atlas_release_frees_for_reuse() {
1684 let mut atlas = TextureWindowAtlas::<1>::new();
1685 let first = atlas.allocate(32, 32).unwrap();
1686 let _second = atlas.allocate(32, 32).unwrap();
1687 atlas.release(first);
1688 let reused = atlas.allocate(32, 32).unwrap();
1689 assert_eq!(
1690 (reused.origin_u(), reused.origin_v()),
1691 (first.origin_u(), first.origin_v()),
1692 "released slot is reused"
1693 );
1694 }
1695
1696 #[test]
1697 fn overlapping_layout_rectangles_are_found_in_listed_order() {
1698 let clean = [
1700 VramRect::new(0, 0, 320, 240),
1701 VramRect::new(0, 240, 320, 240),
1702 VramRect::new(0, 480, 320, 32),
1703 ];
1704 assert_eq!(first_vram_overlap(&clean), None);
1705 assert!(vram_layout_is_disjoint(&clean));
1706
1707 let clashing = [
1710 VramRect::new(0, 0, 320, 240),
1711 VramRect::new(320, 256, 64, 256),
1712 VramRect::new(0, 480, 640, 24),
1713 ];
1714 assert_eq!(first_vram_overlap(&clashing), Some((1, 2)));
1715 assert!(!vram_layout_is_disjoint(&clashing));
1716
1717 let touching = [
1719 VramRect::new(0, 0, 320, 240),
1720 VramRect::new(320, 0, 64, 240),
1721 ];
1722 assert_eq!(first_vram_overlap(&touching), None);
1723 let stacked = [VramRect::new(0, 0, 320, 240), VramRect::new(0, 240, 320, 8)];
1724 assert_eq!(first_vram_overlap(&stacked), None);
1725
1726 assert!(vram_layout_is_disjoint(&[VramRect::new(0, 0, 1, 1)]));
1728 assert!(vram_layout_is_disjoint(&[]));
1729 }
1730}