1use psx_hw::gpu::{gp0, pack_color};
9
10#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub enum BlendMode {
17 Opaque,
19 Average,
21 Add,
23 Subtract,
25 AddQuarter,
27}
28
29#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub struct TextureWindow {
37 mask_x: u8,
38 mask_y: u8,
39 offset_x: u8,
40 offset_y: u8,
41}
42
43impl TextureWindow {
44 pub const NONE: Self = Self::new(0, 0, 0, 0);
46
47 pub const fn new(mask_x: u8, mask_y: u8, offset_x: u8, offset_y: u8) -> Self {
49 assert!(mask_x < 32, "texture-window mask_x must be < 32");
50 assert!(mask_y < 32, "texture-window mask_y must be < 32");
51 assert!(offset_x < 32, "texture-window offset_x must be < 32");
52 assert!(offset_y < 32, "texture-window offset_y must be < 32");
53 Self {
54 mask_x,
55 mask_y,
56 offset_x,
57 offset_y,
58 }
59 }
60
61 pub const fn power_of_two_tile(origin_x: u8, origin_y: u8, size_x: u8, size_y: u8) -> Self {
67 assert!(
68 size_x >= 8 && size_x.is_power_of_two(),
69 "texture-window width must be a power of two >= 8"
70 );
71 assert!(
72 size_y >= 8 && size_y.is_power_of_two(),
73 "texture-window height must be a power of two >= 8"
74 );
75 assert!(
76 origin_x.is_multiple_of(size_x),
77 "texture-window origin_x must align to its width"
78 );
79 assert!(
80 origin_y.is_multiple_of(size_y),
81 "texture-window origin_y must align to its height"
82 );
83 assert!(size_x <= 128, "texture-window width must fit GP0(E2)");
84 assert!(size_y <= 128, "texture-window height must fit GP0(E2)");
85 assert!(
86 origin_x as u16 + size_x as u16 <= 256,
87 "texture window must fit the texture-page width"
88 );
89 assert!(
90 origin_y as u16 + size_y as u16 <= 256,
91 "texture window must fit the texture-page height"
92 );
93 let mask_x = ((!((size_x as u16) - 1)) & 0x00FF) as u8;
94 let mask_y = ((!((size_y as u16) - 1)) & 0x00FF) as u8;
95 Self::new(mask_x / 8, mask_y / 8, origin_x / 8, origin_y / 8)
96 }
97
98 pub const fn word(self) -> u32 {
100 gp0::tex_window(
101 self.mask_x as u32,
102 self.mask_y as u32,
103 self.offset_x as u32,
104 self.offset_y as u32,
105 )
106 }
107
108 pub const fn origin_texels(self) -> [u8; 2] {
114 [self.offset_x * 8, self.offset_y * 8]
115 }
116}
117
118impl BlendMode {
119 #[doc(alias = "tpage")]
125 pub const fn from_texture_page_bits(bits: u8) -> Self {
126 match bits & 0x3 {
127 0 => Self::Average,
128 1 => Self::Add,
129 2 => Self::Subtract,
130 _ => Self::AddQuarter,
131 }
132 }
133
134 #[deprecated(note = "renamed to `from_texture_page_bits`")]
136 #[inline(always)]
137 pub const fn from_tpage_bits(bits: u8) -> Self {
138 Self::from_texture_page_bits(bits)
139 }
140
141 #[doc(alias = "tpage")]
146 pub const fn texture_page_bits(self) -> u8 {
147 match self {
148 Self::Opaque | Self::Average => 0,
149 Self::Add => 1,
150 Self::Subtract => 2,
151 Self::AddQuarter => 3,
152 }
153 }
154
155 pub const fn is_translucent(self) -> bool {
158 !matches!(self, Self::Opaque)
159 }
160}
161
162#[derive(Copy, Clone, Debug, PartialEq, Eq)]
170pub struct TextureMaterial {
171 clut_word: u16,
172 tpage_word: u16,
173 texture_window: TextureWindow,
174 tint: (u8, u8, u8),
175 blend_mode: BlendMode,
176 raw_texture: bool,
177 dither: bool,
178}
179
180#[derive(Copy, Clone, Debug, PartialEq, Eq)]
186pub struct TexturedPacketMaterial {
187 pub tex_window_word: u32,
189 pub color_command_word: u32,
191 pub clut_high_word: u32,
193 pub tpage_high_word: u32,
195}
196
197impl TexturedPacketMaterial {
198 pub const fn from_texture(material: TextureMaterial) -> Self {
200 Self {
201 tex_window_word: material.texture_window_word(),
202 color_command_word: material.flat_textured_polygon_header(false),
203 clut_high_word: (material.clut_word() as u32) << 16,
204 tpage_high_word: (material.texture_page_word() as u32) << 16,
205 }
206 }
207
208 pub const fn is_translucent(self) -> bool {
210 self.color_command_word & (1 << 25) != 0
211 }
212
213 pub const fn with_clut_bank(mut self, bank: u8) -> Self {
215 self.clut_high_word = self.clut_high_word.wrapping_add(((bank & 3) as u32) << 16);
216 self
217 }
218}
219
220#[derive(Copy, Clone, Debug, PartialEq, Eq)]
227pub struct TexturedGouraudPacketMaterial {
228 pub tex_window_word: u32,
230 pub color0_command_word: u32,
232 pub clut_high_word: u32,
234 pub tpage_high_word: u32,
236}
237
238impl TexturedGouraudPacketMaterial {
239 pub const fn from_texture(material: TextureMaterial) -> Self {
241 Self {
242 tex_window_word: material.texture_window_word(),
243 color0_command_word: material.textured_polygon_command(true, false),
244 clut_high_word: (material.clut_word() as u32) << 16,
245 tpage_high_word: (material.texture_page_word() as u32) << 16,
246 }
247 }
248
249 pub const fn is_translucent(self) -> bool {
251 self.color0_command_word & (1 << 25) != 0
252 }
253}
254
255impl TextureMaterial {
256 pub const fn new(clut_word: u16, tpage_word: u16) -> Self {
258 Self::opaque(clut_word, tpage_word, (0x80, 0x80, 0x80))
259 }
260
261 pub const fn opaque(clut_word: u16, tpage_word: u16, tint: (u8, u8, u8)) -> Self {
263 Self {
264 clut_word,
265 tpage_word,
266 texture_window: TextureWindow::NONE,
267 tint,
268 blend_mode: BlendMode::Opaque,
269 raw_texture: false,
270 dither: false,
271 }
272 }
273
274 pub const fn blended(
280 clut_word: u16,
281 tpage_word: u16,
282 tint: (u8, u8, u8),
283 blend_mode: BlendMode,
284 ) -> Self {
285 Self {
286 clut_word,
287 tpage_word,
288 texture_window: TextureWindow::NONE,
289 tint,
290 blend_mode,
291 raw_texture: false,
292 dither: false,
293 }
294 }
295
296 pub const fn with_blend_mode(mut self, blend_mode: BlendMode) -> Self {
304 self.blend_mode = blend_mode;
305 self
306 }
307
308 pub const fn with_tint(mut self, tint: (u8, u8, u8)) -> Self {
310 self.tint = tint;
311 self
312 }
313
314 pub const fn with_clut_bank(mut self, bank: u8) -> Self {
316 self.clut_word = self.clut_word.wrapping_add((bank & 3) as u16);
317 self
318 }
319
320 pub const fn with_texture_window(mut self, texture_window: TextureWindow) -> Self {
322 self.texture_window = texture_window;
323 self
324 }
325
326 pub const fn with_raw_texture(mut self, raw_texture: bool) -> Self {
331 self.raw_texture = raw_texture;
332 self
333 }
334
335 pub const fn with_dither(mut self, dither: bool) -> Self {
337 self.dither = dither;
338 self
339 }
340
341 pub const fn clut_word(self) -> u16 {
343 self.clut_word
344 }
345
346 #[doc(alias = "tpage")]
351 #[doc(alias = "getTPage")]
352 pub const fn texture_page_word(self) -> u16 {
353 let blend_bits = (self.blend_mode.texture_page_bits() as u16) << 5;
354 let dither_bit = (self.dither as u16) << 9;
355 (self.tpage_word & !(0x0060 | 0x0200)) | blend_bits | dither_bit
356 }
357
358 pub const fn texture_window_word(self) -> u32 {
360 self.texture_window.word()
361 }
362
363 pub const fn tint(self) -> (u8, u8, u8) {
365 self.tint
366 }
367
368 pub const fn blend_mode(self) -> BlendMode {
370 self.blend_mode
371 }
372
373 pub const fn is_translucent(self) -> bool {
375 self.blend_mode.is_translucent()
376 }
377
378 pub const fn is_raw_texture(self) -> bool {
380 self.raw_texture
381 }
382
383 pub const fn is_dithered(self) -> bool {
385 self.dither
386 }
387
388 pub const fn textured_polygon_command(self, gouraud: bool, quad: bool) -> u32 {
390 gp0::polygon_opcode(gouraud, quad, true, self.is_translucent(), self.raw_texture)
391 }
392
393 pub const fn textured_gouraud_packet_material(self) -> TexturedGouraudPacketMaterial {
395 TexturedGouraudPacketMaterial::from_texture(self)
396 }
397
398 pub const fn textured_packet_material(self) -> TexturedPacketMaterial {
400 TexturedPacketMaterial::from_texture(self)
401 }
402
403 pub const fn flat_textured_polygon_header(self, quad: bool) -> u32 {
405 let (r, g, b) = self.tint;
406 self.textured_polygon_command(false, quad) | pack_color(r, g, b)
407 }
408
409 pub const fn textured_rect_header(self) -> u32 {
411 let (r, g, b) = self.tint;
412 psx_hw::gpu::packet::TEXTURED_RECT
413 | ((self.is_translucent() as u32) << 25)
414 | ((self.raw_texture as u32) << 24)
415 | pack_color(r, g, b)
416 }
417
418 pub const fn draw_mode_word(self) -> u32 {
425 let tpage = self.texture_page_word();
426 gp0::draw_mode(
427 (tpage & 0x0F) as u32,
428 ((tpage >> 4) & 1) as u32,
429 self.blend_mode.texture_page_bits() as u32,
430 ((tpage >> 7) & 0x3) as u32,
431 self.dither,
432 true,
433 )
434 }
435
436 #[deprecated(note = "use `Gpu::set_draw_mode`")]
438 pub fn apply_draw_mode(self) {
439 crate::gpu::set_draw_mode(self);
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::{BlendMode, TextureMaterial, TextureWindow};
446
447 #[test]
448 fn blend_mode_decodes_tpage_bits() {
449 assert_eq!(BlendMode::from_texture_page_bits(0), BlendMode::Average);
450 assert_eq!(BlendMode::from_texture_page_bits(1), BlendMode::Add);
451 assert_eq!(BlendMode::from_texture_page_bits(2), BlendMode::Subtract);
452 assert_eq!(BlendMode::from_texture_page_bits(3), BlendMode::AddQuarter);
453 assert_eq!(BlendMode::from_texture_page_bits(4), BlendMode::Average);
454 }
455
456 #[test]
457 #[should_panic(expected = "texture-window origin_x must align to its width")]
458 fn power_of_two_window_rejects_an_origin_only_aligned_to_eight_texels() {
459 let _ = TextureWindow::power_of_two_tile(8, 0, 32, 32);
460 }
461
462 #[test]
463 fn material_encodes_blend_dither_and_command_bits() {
464 let material =
465 TextureMaterial::blended(0x1234, 0x018F, (0x80, 0x40, 0x20), BlendMode::AddQuarter)
466 .with_texture_window(super::TextureWindow::power_of_two_tile(64, 64, 64, 64))
467 .with_raw_texture(true)
468 .with_dither(true);
469
470 assert_eq!(material.clut_word(), 0x1234);
471 assert_eq!((material.texture_page_word() >> 5) & 0x3, 3);
472 assert_eq!((material.texture_page_word() >> 9) & 0x1, 1);
473 assert_eq!(material.texture_window_word(), 0xE204_2318);
474 assert_eq!(material.texture_window.origin_texels(), [64, 64]);
475 assert_eq!((material.textured_rect_header() >> 24) & 0xFF, 0x67);
476 assert_eq!(
477 (material.flat_textured_polygon_header(true) >> 24) & 0xFF,
478 0x2F
479 );
480 }
481
482 #[test]
483 fn gouraud_packet_material_is_compact_and_tracks_translucency() {
484 let opaque = TextureMaterial::opaque(0x1234, 0x018f, (0x80, 0x80, 0x80))
485 .textured_gouraud_packet_material();
486 let translucent =
487 TextureMaterial::blended(0x1234, 0x018f, (0x80, 0x80, 0x80), BlendMode::Average)
488 .textured_gouraud_packet_material();
489
490 assert_eq!(
491 core::mem::size_of::<super::TexturedGouraudPacketMaterial>(),
492 16
493 );
494 assert!(!opaque.is_translucent());
495 assert!(translucent.is_translucent());
496 }
497}