embedded-3dgfx 0.1.0

3D graphics rendering for embedded systems (fork of embedded-gfx by Kezii)
Documentation
//! Texture mapping support for embedded 3D graphics
//!
//! This module provides texture storage, sampling, and management for UV-mapped
//! 3D rendering. It uses static texture data and power-of-2 dimensions for
//! efficient wrapping without divisions.

use embedded_graphics_core::pixelcolor::Rgb565;
use heapless::Vec as HeaplessVec;

/// A 2D texture with RGB565 pixel data
///
/// Textures must have power-of-2 dimensions (8, 16, 32, 64, 128, 256, etc.)
/// for efficient wrapping using bit masks instead of modulo operations.
#[derive(Debug, Clone, Copy)]
pub struct Texture {
    /// Texture pixel data in RGB565 format
    pub data: &'static [Rgb565],
    /// Width of the texture (must be power of 2)
    pub width: u32,
    /// Height of the texture (must be power of 2)
    pub height: u32,
    /// Bit mask for wrapping width (width - 1)
    width_mask: u32,
    /// Bit mask for wrapping height (height - 1)
    height_mask: u32,
}

impl Texture {
    /// Create a new texture
    ///
    /// # Arguments
    /// * `data` - Static RGB565 pixel array (must be width × height elements)
    /// * `width` - Texture width (must be power of 2)
    /// * `height` - Texture height (must be power of 2)
    ///
    /// # Panics
    /// Panics if width or height is not a power of 2, or if data length doesn't match dimensions
    pub fn new(data: &'static [Rgb565], width: u32, height: u32) -> Self {
        assert!(width.is_power_of_two(), "Texture width must be power of 2");
        assert!(
            height.is_power_of_two(),
            "Texture height must be power of 2"
        );
        assert_eq!(
            data.len(),
            (width * height) as usize,
            "Texture data length must match width × height"
        );

        Self {
            data,
            width,
            height,
            width_mask: width - 1,
            height_mask: height - 1,
        }
    }

    /// Sample the texture at UV coordinates
    ///
    /// Uses nearest-neighbor sampling with wrapping (repeat mode).
    /// UV coordinates are in the range [0.0, 1.0] where:
    /// - (0, 0) is the top-left corner
    /// - (1, 1) is the bottom-right corner
    ///
    /// # Arguments
    /// * `u` - Horizontal texture coordinate (0.0-1.0+, wraps)
    /// * `v` - Vertical texture coordinate (0.0-1.0+, wraps)
    #[inline]
    pub fn sample(&self, u: f32, v: f32) -> Rgb565 {
        // Convert UV [0.0, 1.0] to texture coordinates [0, width/height)
        let tex_x = (u * self.width as f32) as u32;
        let tex_y = (v * self.height as f32) as u32;

        // Wrap coordinates using bit masks (fast for power-of-2 dimensions)
        let tex_x = tex_x & self.width_mask;
        let tex_y = tex_y & self.height_mask;

        // Lookup pixel
        self.data[(tex_y * self.width + tex_x) as usize]
    }

    /// Sample the texture at UV coordinates (integer version for performance)
    ///
    /// Uses fixed-point UV coordinates (16.16 format) for faster inner loops.
    ///
    /// # Arguments
    /// * `u_fixed` - Horizontal texture coordinate in 16.16 fixed-point
    /// * `v_fixed` - Vertical texture coordinate in 16.16 fixed-point
    #[inline]
    pub fn sample_fixed(&self, u_fixed: u32, v_fixed: u32) -> Rgb565 {
        // Convert from 16.16 fixed-point to texture coordinates
        // Shift right by 16 to get integer part, then multiply by width/height
        let tex_x = ((u_fixed >> 16) * self.width) >> 16;
        let tex_y = ((v_fixed >> 16) * self.height) >> 16;

        // Wrap coordinates
        let tex_x = tex_x & self.width_mask;
        let tex_y = tex_y & self.height_mask;

        self.data[(tex_y * self.width + tex_x) as usize]
    }

    /// Get texture dimensions
    pub fn dimensions(&self) -> (u32, u32) {
        (self.width, self.height)
    }
}

/// Texture manager for storing multiple textures
///
/// Uses a fixed-size heapless vector for no_std compatibility.
/// The capacity N determines how many textures can be stored.
pub struct TextureManager<const N: usize> {
    textures: HeaplessVec<Texture, N>,
}

impl<const N: usize> TextureManager<N> {
    /// Create a new empty texture manager
    pub fn new() -> Self {
        Self {
            textures: HeaplessVec::new(),
        }
    }

    /// Add a texture to the manager
    ///
    /// Returns the texture ID (index) that can be used to reference this texture.
    ///
    /// # Returns
    /// `Some(texture_id)` if successful, `None` if the manager is full
    pub fn add_texture(&mut self, texture: Texture) -> Option<u32> {
        self.textures.push(texture).ok()?;
        Some((self.textures.len() - 1) as u32)
    }

    /// Get a texture by ID
    ///
    /// # Arguments
    /// * `id` - Texture ID returned by `add_texture()`
    ///
    /// # Returns
    /// `Some(&Texture)` if the ID is valid, `None` otherwise
    pub fn get(&self, id: u32) -> Option<&Texture> {
        self.textures.get(id as usize)
    }

    /// Get the number of stored textures
    pub fn len(&self) -> usize {
        self.textures.len()
    }

    /// Check if the manager is empty
    pub fn is_empty(&self) -> bool {
        self.textures.is_empty()
    }

    /// Check if the manager is full
    pub fn is_full(&self) -> bool {
        self.textures.len() >= N
    }
}

impl<const N: usize> Default for TextureManager<N> {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    extern crate std;
    use super::*;
    use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};

    #[test]
    fn test_texture_creation() {
        static DATA: [Rgb565; 64] = [Rgb565::CSS_RED; 64];
        let texture = Texture::new(&DATA, 8, 8);

        assert_eq!(texture.width, 8);
        assert_eq!(texture.height, 8);
        assert_eq!(texture.dimensions(), (8, 8));
    }

    #[test]
    #[should_panic(expected = "width must be power of 2")]
    fn test_texture_non_power_of_2_width() {
        static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
        let _texture = Texture::new(&DATA, 10, 6); // 10 is not power of 2
    }

    #[test]
    #[should_panic(expected = "height must be power of 2")]
    fn test_texture_non_power_of_2_height() {
        static DATA: [Rgb565; 48] = [Rgb565::CSS_RED; 48];
        let _texture = Texture::new(&DATA, 8, 6); // 6 is not power of 2
    }

    #[test]
    #[should_panic(expected = "length must match")]
    fn test_texture_wrong_data_length() {
        static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
        let _texture = Texture::new(&DATA, 8, 8); // Should be 64 elements
    }

    #[test]
    fn test_texture_sampling() {
        static DATA: [Rgb565; 16] = [
            Rgb565::CSS_RED,
            Rgb565::CSS_GREEN,
            Rgb565::CSS_BLUE,
            Rgb565::CSS_YELLOW,
            Rgb565::CSS_CYAN,
            Rgb565::CSS_MAGENTA,
            Rgb565::CSS_WHITE,
            Rgb565::CSS_BLACK,
            Rgb565::CSS_RED,
            Rgb565::CSS_GREEN,
            Rgb565::CSS_BLUE,
            Rgb565::CSS_YELLOW,
            Rgb565::CSS_CYAN,
            Rgb565::CSS_MAGENTA,
            Rgb565::CSS_WHITE,
            Rgb565::CSS_BLACK,
        ];

        let texture = Texture::new(&DATA, 4, 4);

        // Sample at corners
        let tl = texture.sample(0.0, 0.0);
        assert_eq!(tl, Rgb565::CSS_RED);

        // Sample in middle (0.5, 0.5) -> (2, 2) -> index 10
        let mid = texture.sample(0.5, 0.5);
        assert_eq!(mid, Rgb565::CSS_BLUE);
    }

    #[test]
    fn test_texture_wrapping() {
        static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
        let texture = Texture::new(&DATA, 4, 4);

        // Sample beyond 1.0 should wrap
        let wrapped = texture.sample(1.5, 1.5);
        assert_eq!(wrapped, Rgb565::CSS_RED);
    }

    #[test]
    fn test_texture_manager() {
        static DATA1: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
        static DATA2: [Rgb565; 64] = [Rgb565::CSS_GREEN; 64];

        let mut manager = TextureManager::<4>::new();

        assert!(manager.is_empty());
        assert!(!manager.is_full());

        let id1 = manager.add_texture(Texture::new(&DATA1, 4, 4));
        assert_eq!(id1, Some(0));
        assert_eq!(manager.len(), 1);

        let id2 = manager.add_texture(Texture::new(&DATA2, 8, 8));
        assert_eq!(id2, Some(1));
        assert_eq!(manager.len(), 2);

        // Retrieve textures
        let tex1 = manager.get(0).unwrap();
        assert_eq!(tex1.width, 4);

        let tex2 = manager.get(1).unwrap();
        assert_eq!(tex2.width, 8);
    }

    #[test]
    fn test_texture_manager_full() {
        static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];

        let mut manager = TextureManager::<2>::new();

        // Fill the manager
        assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
        assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
        assert!(manager.is_full());

        // Try to add one more (should fail)
        assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_none());
    }
}