1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
8use heapless::Vec as HeaplessVec;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TextureFormat {
12 Rgb565,
13 Palettized8,
14 Palettized4,
15}
16
17#[derive(Debug, Clone, Copy)]
22pub struct Texture {
23 pub data: &'static [Rgb565],
25 pub width: u32,
27 pub height: u32,
29 width_mask: u32,
31 height_mask: u32,
33 pub palette: &'static [Rgb565],
35 pub indices: &'static [u8],
37 pub format: TextureFormat,
39}
40
41impl Texture {
42 pub fn new(data: &'static [Rgb565], width: u32, height: u32) -> Self {
52 assert!(width.is_power_of_two(), "Texture width must be power of 2");
53 assert!(
54 height.is_power_of_two(),
55 "Texture height must be power of 2"
56 );
57 assert_eq!(
58 data.len(),
59 (width * height) as usize,
60 "Texture data length must match width × height"
61 );
62
63 Self {
64 data,
65 width,
66 height,
67 width_mask: width - 1,
68 height_mask: height - 1,
69 palette: &[],
70 indices: &[],
71 format: TextureFormat::Rgb565,
72 }
73 }
74
75 pub fn new_palettized8(
77 indices: &'static [u8],
78 palette: &'static [Rgb565],
79 width: u32,
80 height: u32,
81 ) -> Self {
82 assert!(width.is_power_of_two(), "Texture width must be power of 2");
83 assert!(
84 height.is_power_of_two(),
85 "Texture height must be power of 2"
86 );
87 assert_eq!(
88 indices.len(),
89 (width * height) as usize,
90 "Indices length must match width × height"
91 );
92 assert!(palette.len() <= 256, "Palette cannot exceed 256 colors");
93
94 Self {
95 data: &[],
96 width,
97 height,
98 width_mask: width - 1,
99 height_mask: height - 1,
100 palette,
101 indices,
102 format: TextureFormat::Palettized8,
103 }
104 }
105
106 pub fn new_palettized4(
108 indices: &'static [u8],
109 palette: &'static [Rgb565],
110 width: u32,
111 height: u32,
112 ) -> Self {
113 assert!(width.is_power_of_two(), "Texture width must be power of 2");
114 assert!(
115 height.is_power_of_two(),
116 "Texture height must be power of 2"
117 );
118 assert_eq!(
119 indices.len(),
120 ((width * height + 1) / 2) as usize,
121 "Indices length must match packed width × height / 2"
122 );
123 assert!(palette.len() <= 16, "Palette cannot exceed 16 colors");
124
125 Self {
126 data: &[],
127 width,
128 height,
129 width_mask: width - 1,
130 height_mask: height - 1,
131 palette,
132 indices,
133 format: TextureFormat::Palettized4,
134 }
135 }
136
137 #[inline(always)]
139 pub fn lookup_pixel(&self, tex_x: u32, tex_y: u32) -> Rgb565 {
140 match self.format {
141 TextureFormat::Rgb565 => self.data[(tex_y * self.width + tex_x) as usize],
142 TextureFormat::Palettized8 => {
143 let idx = self.indices[(tex_y * self.width + tex_x) as usize] as usize;
144 if idx < self.palette.len() {
145 self.palette[idx]
146 } else {
147 Rgb565::new(0, 0, 0)
148 }
149 }
150 TextureFormat::Palettized4 => {
151 let pixel_idx = (tex_y * self.width + tex_x) as usize;
152 let byte_idx = pixel_idx / 2;
153 let shift = if pixel_idx % 2 == 0 { 4 } else { 0 };
154 let val = self.indices[byte_idx];
155 let palette_idx = ((val >> shift) & 0x0F) as usize;
156 if palette_idx < self.palette.len() {
157 self.palette[palette_idx]
158 } else {
159 Rgb565::new(0, 0, 0)
160 }
161 }
162 }
163 }
164
165 #[inline]
176 pub fn sample(&self, u: f32, v: f32) -> Rgb565 {
177 let tex_x = (u * self.width as f32) as u32;
179 let tex_y = (v * self.height as f32) as u32;
180
181 let tex_x = tex_x & self.width_mask;
183 let tex_y = tex_y & self.height_mask;
184
185 self.lookup_pixel(tex_x, tex_y)
187 }
188
189 #[inline]
197 pub fn sample_fixed(&self, u_fixed: u32, v_fixed: u32) -> Rgb565 {
198 let tex_x = ((u_fixed >> 16) * self.width) >> 16;
201 let tex_y = ((v_fixed >> 16) * self.height) >> 16;
202
203 let tex_x = tex_x & self.width_mask;
205 let tex_y = tex_y & self.height_mask;
206
207 self.lookup_pixel(tex_x, tex_y)
208 }
209
210 #[inline(always)]
214 pub fn sample_affine_q16(&self, u_q16: u32, v_q16: u32) -> Rgb565 {
215 let tex_x = (((u_q16 as u64 * self.width as u64) >> 16) as u32) & self.width_mask;
216 let tex_y = (((v_q16 as u64 * self.height as u64) >> 16) as u32) & self.height_mask;
217 self.lookup_pixel(tex_x, tex_y)
218 }
219
220 #[inline]
225 pub fn sample_affine_2xssaa_q16(&self, u_q16: u32, v_q16: u32) -> Rgb565 {
226 const OFF: u32 = 0x4000; let p0 = self.sample_affine_q16(u_q16.wrapping_sub(OFF), v_q16.wrapping_sub(OFF));
228 let p1 = self.sample_affine_q16(u_q16.wrapping_add(OFF), v_q16.wrapping_sub(OFF));
229 let p2 = self.sample_affine_q16(u_q16.wrapping_sub(OFF), v_q16.wrapping_add(OFF));
230 let p3 = self.sample_affine_q16(u_q16.wrapping_add(OFF), v_q16.wrapping_add(OFF));
231
232 let r = (p0.r() as u32 + p1.r() as u32 + p2.r() as u32 + p3.r() as u32 + 2) >> 2;
233 let g = (p0.g() as u32 + p1.g() as u32 + p2.g() as u32 + p3.g() as u32 + 2) >> 2;
234 let b = (p0.b() as u32 + p1.b() as u32 + p2.b() as u32 + p3.b() as u32 + 2) >> 2;
235
236 Rgb565::new(r as u8, g as u8, b as u8)
237 }
238
239 pub fn sample_affine_scanline_q16(
241 &self,
242 mut u_q16: u32,
243 mut v_q16: u32,
244 du_q16: u32,
245 dv_q16: u32,
246 out_buffer: &mut [Rgb565],
247 ) {
248 for pixel in out_buffer.iter_mut() {
249 *pixel = self.sample_affine_q16(u_q16, v_q16);
250 u_q16 = u_q16.wrapping_add(du_q16);
251 v_q16 = v_q16.wrapping_add(dv_q16);
252 }
253 }
254
255 pub fn dimensions(&self) -> (u32, u32) {
257 (self.width, self.height)
258 }
259}
260
261const ANIMATION_ID_FLAG: u32 = 0x8000_0000;
262const ANIMATION_ID_MASK: u32 = !ANIMATION_ID_FLAG;
263const MAX_ANIMATIONS: usize = 16;
264const MAX_ANIMATION_FRAMES: usize = 8;
265
266#[derive(Debug, Clone, Copy)]
267struct TextureAnimation {
268 frames: [u32; MAX_ANIMATION_FRAMES],
269 frame_count: u8,
270 ticks_per_frame: u16,
271 tick_accum: u16,
272 current_frame: u8,
273 looping: bool,
274}
275
276impl TextureAnimation {
277 fn new(frame_ids: &[u32], ticks_per_frame: u16, looping: bool) -> Option<Self> {
278 if frame_ids.is_empty() || frame_ids.len() > MAX_ANIMATION_FRAMES {
279 return None;
280 }
281 let mut frames = [0u32; MAX_ANIMATION_FRAMES];
282 for (i, frame_id) in frame_ids.iter().copied().enumerate() {
283 frames[i] = frame_id;
284 }
285 Some(Self {
286 frames,
287 frame_count: frame_ids.len() as u8,
288 ticks_per_frame: ticks_per_frame.max(1),
289 tick_accum: 0,
290 current_frame: 0,
291 looping,
292 })
293 }
294
295 #[inline]
296 fn current_texture_id(&self) -> u32 {
297 self.frames[self.current_frame as usize]
298 }
299
300 fn tick(&mut self, ticks: u16) {
301 if self.frame_count <= 1 {
302 return;
303 }
304 let mut accum = self.tick_accum.saturating_add(ticks);
305 while accum >= self.ticks_per_frame {
306 accum -= self.ticks_per_frame;
307 if self.current_frame + 1 < self.frame_count {
308 self.current_frame += 1;
309 } else if self.looping {
310 self.current_frame = 0;
311 } else {
312 accum = 0;
314 break;
315 }
316 }
317 self.tick_accum = accum;
318 }
319}
320
321pub struct TextureManager<const N: usize> {
326 textures: HeaplessVec<Texture, N>,
327 animations: HeaplessVec<TextureAnimation, MAX_ANIMATIONS>,
328}
329
330impl<const N: usize> TextureManager<N> {
331 pub fn new() -> Self {
333 Self {
334 textures: HeaplessVec::new(),
335 animations: HeaplessVec::new(),
336 }
337 }
338
339 pub fn add_texture(&mut self, texture: Texture) -> Option<u32> {
346 self.textures.push(texture).ok()?;
347 Some((self.textures.len() - 1) as u32)
348 }
349
350 pub fn get(&self, id: u32) -> Option<&Texture> {
358 let resolved = self.resolve_texture_id(id)?;
359 self.textures.get(resolved as usize)
360 }
361
362 pub fn add_animation(
366 &mut self,
367 frame_ids: &[u32],
368 ticks_per_frame: u16,
369 looping: bool,
370 ) -> Option<u32> {
371 for frame_id in frame_ids {
373 let resolved = self.resolve_texture_id(*frame_id)?;
374 if resolved as usize >= self.textures.len() {
375 return None;
376 }
377 }
378
379 let animation = TextureAnimation::new(frame_ids, ticks_per_frame, looping)?;
380 self.animations.push(animation).ok()?;
381 let index = (self.animations.len() - 1) as u32;
382 Some(ANIMATION_ID_FLAG | (index & ANIMATION_ID_MASK))
383 }
384
385 pub fn tick(&mut self, ticks: u16) {
387 for animation in &mut self.animations {
388 animation.tick(ticks);
389 }
390 }
391
392 #[inline]
394 pub fn is_animation_id(id: u32) -> bool {
395 (id & ANIMATION_ID_FLAG) != 0
396 }
397
398 pub fn resolve_texture_id(&self, id: u32) -> Option<u32> {
403 if !Self::is_animation_id(id) {
404 return Some(id);
405 }
406 let anim_idx = (id & ANIMATION_ID_MASK) as usize;
407 let animation = self.animations.get(anim_idx)?;
408 Some(animation.current_texture_id())
409 }
410
411 pub fn len(&self) -> usize {
413 self.textures.len()
414 }
415
416 pub fn is_empty(&self) -> bool {
418 self.textures.is_empty()
419 }
420
421 pub fn is_full(&self) -> bool {
423 self.textures.len() >= N
424 }
425}
426
427impl<const N: usize> Default for TextureManager<N> {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 extern crate std;
436 use super::*;
437 use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
438
439 #[test]
440 fn test_texture_creation() {
441 static DATA: [Rgb565; 64] = [Rgb565::CSS_RED; 64];
442 let texture = Texture::new(&DATA, 8, 8);
443
444 assert_eq!(texture.width, 8);
445 assert_eq!(texture.height, 8);
446 assert_eq!(texture.dimensions(), (8, 8));
447 }
448
449 #[test]
450 #[should_panic(expected = "width must be power of 2")]
451 fn test_texture_non_power_of_2_width() {
452 static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
453 let _texture = Texture::new(&DATA, 10, 6); }
455
456 #[test]
457 #[should_panic(expected = "height must be power of 2")]
458 fn test_texture_non_power_of_2_height() {
459 static DATA: [Rgb565; 48] = [Rgb565::CSS_RED; 48];
460 let _texture = Texture::new(&DATA, 8, 6); }
462
463 #[test]
464 #[should_panic(expected = "length must match")]
465 fn test_texture_wrong_data_length() {
466 static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
467 let _texture = Texture::new(&DATA, 8, 8); }
469
470 #[test]
471 fn test_texture_sampling() {
472 static DATA: [Rgb565; 16] = [
473 Rgb565::CSS_RED,
474 Rgb565::CSS_GREEN,
475 Rgb565::CSS_BLUE,
476 Rgb565::CSS_YELLOW,
477 Rgb565::CSS_CYAN,
478 Rgb565::CSS_MAGENTA,
479 Rgb565::CSS_WHITE,
480 Rgb565::CSS_BLACK,
481 Rgb565::CSS_RED,
482 Rgb565::CSS_GREEN,
483 Rgb565::CSS_BLUE,
484 Rgb565::CSS_YELLOW,
485 Rgb565::CSS_CYAN,
486 Rgb565::CSS_MAGENTA,
487 Rgb565::CSS_WHITE,
488 Rgb565::CSS_BLACK,
489 ];
490
491 let texture = Texture::new(&DATA, 4, 4);
492
493 let tl = texture.sample(0.0, 0.0);
495 assert_eq!(tl, Rgb565::CSS_RED);
496
497 let mid = texture.sample(0.5, 0.5);
499 assert_eq!(mid, Rgb565::CSS_BLUE);
500 }
501
502 #[test]
503 fn test_texture_wrapping() {
504 static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
505 let texture = Texture::new(&DATA, 4, 4);
506
507 let wrapped = texture.sample(1.5, 1.5);
509 assert_eq!(wrapped, Rgb565::CSS_RED);
510 }
511
512 #[test]
513 fn test_texture_manager() {
514 static DATA1: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
515 static DATA2: [Rgb565; 64] = [Rgb565::CSS_GREEN; 64];
516
517 let mut manager = TextureManager::<4>::new();
518
519 assert!(manager.is_empty());
520 assert!(!manager.is_full());
521
522 let id1 = manager.add_texture(Texture::new(&DATA1, 4, 4));
523 assert_eq!(id1, Some(0));
524 assert_eq!(manager.len(), 1);
525
526 let id2 = manager.add_texture(Texture::new(&DATA2, 8, 8));
527 assert_eq!(id2, Some(1));
528 assert_eq!(manager.len(), 2);
529
530 let tex1 = manager.get(0).unwrap();
532 assert_eq!(tex1.width, 4);
533
534 let tex2 = manager.get(1).unwrap();
535 assert_eq!(tex2.width, 8);
536 }
537
538 #[test]
539 fn test_texture_manager_full() {
540 static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
541
542 let mut manager = TextureManager::<2>::new();
543
544 assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
546 assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
547 assert!(manager.is_full());
548
549 assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_none());
551 }
552
553 #[test]
554 fn test_texture_animation_looping_sequence() {
555 static RED: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
556 static GREEN: [Rgb565; 16] = [Rgb565::CSS_GREEN; 16];
557 static BLUE: [Rgb565; 16] = [Rgb565::CSS_BLUE; 16];
558
559 let mut manager = TextureManager::<8>::new();
560 let red = manager.add_texture(Texture::new(&RED, 4, 4)).unwrap();
561 let green = manager.add_texture(Texture::new(&GREEN, 4, 4)).unwrap();
562 let blue = manager.add_texture(Texture::new(&BLUE, 4, 4)).unwrap();
563
564 let anim_id = manager.add_animation(&[red, green, blue], 2, true).unwrap();
565 assert!(TextureManager::<8>::is_animation_id(anim_id));
566
567 assert_eq!(
568 manager.get(anim_id).unwrap().sample(0.0, 0.0),
569 Rgb565::CSS_RED
570 );
571 manager.tick(2);
572 assert_eq!(
573 manager.get(anim_id).unwrap().sample(0.0, 0.0),
574 Rgb565::CSS_GREEN
575 );
576 manager.tick(2);
577 assert_eq!(
578 manager.get(anim_id).unwrap().sample(0.0, 0.0),
579 Rgb565::CSS_BLUE
580 );
581 manager.tick(2);
582 assert_eq!(
583 manager.get(anim_id).unwrap().sample(0.0, 0.0),
584 Rgb565::CSS_RED
585 );
586 }
587
588 #[test]
589 fn test_texture_animation_non_looping_clamps_final_frame() {
590 static RED: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
591 static GREEN: [Rgb565; 16] = [Rgb565::CSS_GREEN; 16];
592
593 let mut manager = TextureManager::<4>::new();
594 let red = manager.add_texture(Texture::new(&RED, 4, 4)).unwrap();
595 let green = manager.add_texture(Texture::new(&GREEN, 4, 4)).unwrap();
596
597 let anim_id = manager.add_animation(&[red, green], 1, false).unwrap();
598 manager.tick(1);
599 assert_eq!(
600 manager.get(anim_id).unwrap().sample(0.0, 0.0),
601 Rgb565::CSS_GREEN
602 );
603 manager.tick(10);
604 assert_eq!(
605 manager.get(anim_id).unwrap().sample(0.0, 0.0),
606 Rgb565::CSS_GREEN
607 );
608 }
609
610 #[test]
611 fn test_texture_sample_affine_q16() {
612 static DATA: [Rgb565; 16] = [
613 Rgb565::CSS_RED,
614 Rgb565::CSS_GREEN,
615 Rgb565::CSS_BLUE,
616 Rgb565::CSS_YELLOW,
617 Rgb565::CSS_CYAN,
618 Rgb565::CSS_MAGENTA,
619 Rgb565::CSS_WHITE,
620 Rgb565::CSS_BLACK,
621 Rgb565::CSS_RED,
622 Rgb565::CSS_GREEN,
623 Rgb565::CSS_BLUE,
624 Rgb565::CSS_YELLOW,
625 Rgb565::CSS_CYAN,
626 Rgb565::CSS_MAGENTA,
627 Rgb565::CSS_WHITE,
628 Rgb565::CSS_BLACK,
629 ];
630 let texture = Texture::new(&DATA, 4, 4);
631
632 let sample = texture.sample_affine_q16(32768, 32768);
634 assert_eq!(sample, Rgb565::CSS_BLUE);
635 }
636
637 #[test]
638 fn test_texture_sample_affine_scanline_q16() {
639 static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
640 let texture = Texture::new(&DATA, 4, 4);
641
642 let mut scanline = [Rgb565::CSS_BLACK; 4];
643 texture.sample_affine_scanline_q16(0, 0, 16384, 0, &mut scanline);
644 assert_eq!(scanline[0], Rgb565::CSS_RED);
645 }
646}