gizmo-renderer 0.8.0

A custom ECS and physics engine aimed for realistic simulations.
Documentation
use super::decode_rgba_image_file;
use super::error::AssetError;
use std::sync::Arc;

// ============================================================================
//  Shared sampler descriptors
// ============================================================================

/// Standard sampler for real textures: bilinear, repeating.
/// Mipmap filter is Nearest because we only allocate one mip level —
/// using Linear here would trigger a wgpu validation warning.
const SAMPLER_LINEAR_REPEAT: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor {
    label: Some("linear_repeat_sampler"),
    address_mode_u: wgpu::AddressMode::Repeat,
    address_mode_v: wgpu::AddressMode::Repeat,
    address_mode_w: wgpu::AddressMode::Repeat,
    mag_filter: wgpu::FilterMode::Linear,
    min_filter: wgpu::FilterMode::Linear,
    mipmap_filter: wgpu::MipmapFilterMode::Nearest, // single mip — must be Nearest
    lod_min_clamp: 0.0,
    lod_max_clamp: 0.0,
    compare: None,
    anisotropy_clamp: 1,
    border_color: None,
};

// ============================================================================
//  AssetManager — texture methods
// ============================================================================

impl super::AssetManager {
    // ── Internal helpers ──────────────────────────────────────────────────

    /// Resolve a `path_or_uuid` argument to the string key used in
    /// `texture_cache`.  Returns `(resolved_fs_path, cache_key)`.
    ///
    /// The cache key is the UUID string when one is registered, otherwise
    /// the normalised filesystem path.  Keeping cache keys stable across
    /// renames is why UUIDs are preferred.
    fn resolve_texture_cache_key(
        &self,
        path_or_uuid: &str,
    ) -> Result<(String, String), AssetError> {
        let resolved = self.resolve_path_from_meta_source(path_or_uuid)?;
        let cache_key = self
            .get_uuid(&resolved)
            .map(|id| id.to_string())
            .unwrap_or_else(|| resolved.clone());
        Ok((resolved, cache_key))
    }

    /// Upload a single RGBA8 pixel buffer to the GPU, cache the bind group,
    /// and return it.
    ///
    /// Called by async loaders after decoding completes on a worker thread,
    /// and by the procedural texture helpers below.
    ///
    /// # Errors
    ///
    /// Returns an error when:
    /// * `width` or `height` is zero (wgpu would panic on a zero-sized texture).
    /// * `rgba.len()` does not equal `width * height * 4`.
    pub fn install_decoded_material_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
        cache_key: &str,
        rgba: &[u8],
        width: u32,
        height: u32,
    ) -> Result<Arc<wgpu::BindGroup>, AssetError> {
        // Guard against zero-sized textures — wgpu panics on Extent3d { width:0, .. }.
        if width == 0 || height == 0 {
            return Err(AssetError::ZeroDimensionTexture {
                cache_key: cache_key.to_string(),
                width,
                height,
            });
        }

        let expected = (width as usize)
            .saturating_mul(height as usize)
            .saturating_mul(4);

        if rgba.len() != expected {
            return Err(AssetError::RgbaSizeMismatch {
                cache_key: cache_key.to_string(),
                got: rgba.len(),
                expected,
                width,
                height,
            });
        }

        let texture_size = wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        };

        let texture = device.create_texture(&wgpu::TextureDescriptor {
            size: texture_size,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            label: Some(cache_key),
            view_formats: &[],
        });

        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            rgba,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(4 * width),
                rows_per_image: Some(height),
            },
            texture_size,
        );

        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let sampler = device.create_sampler(&SAMPLER_LINEAR_REPEAT);
        let bg = self
            .assemble_single_texture_bind_group(device, queue, layout, &view, &sampler, cache_key);
        self.texture_cache.insert(cache_key.to_string(), bg.clone());
        Ok(bg)
    }

    // ── Public load API ───────────────────────────────────────────────────

    /// Load a texture from `path_or_uuid`, uploading it to the GPU on first
    /// access and returning the cached bind group on subsequent calls.
    ///
    /// Supports both filesystem paths and UUID strings registered by the asset
    /// scanner.  Embedded assets (registered with [`AssetManager::embed_asset`])
    /// take priority over filesystem reads.
    pub fn load_material_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
        path_or_uuid: &str,
    ) -> Result<Arc<wgpu::BindGroup>, AssetError> {
        let (resolved_path, cache_key) = self.resolve_texture_cache_key(path_or_uuid)?;

        if let Some(cached) = self.texture_cache.get(&cache_key) {
            return Ok(cached.clone());
        }

        let (rgba, w, h) = self.decode_texture_rgba(&resolved_path)?;
        self.install_decoded_material_texture(device, queue, layout, &cache_key, &rgba, w, h)
    }

    /// Evict `path_or_uuid` from the texture cache and reload it from disk.
    ///
    /// Useful for hot-reload workflows where an asset file changes at runtime.
    pub fn reload_material_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
        path_or_uuid: &str,
    ) -> Result<Arc<wgpu::BindGroup>, AssetError> {
        // Resolve the key first so we evict the correct entry, then reload.
        let (_, cache_key) = self.resolve_texture_cache_key(path_or_uuid)?;
        self.texture_cache.remove(&cache_key);
        self.load_material_texture(device, queue, layout, path_or_uuid)
    }

    // ── Procedural textures ───────────────────────────────────────────────

    /// Return (creating once) a 1×1 opaque-white texture.
    ///
    /// Used as the default albedo map for materials that specify no texture.
    pub fn create_white_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
    ) -> Arc<wgpu::BindGroup> {
        const KEY: &str = "__white_fallback_texture__";

        if let Some(cached) = self.texture_cache.get(KEY) {
            return cached.clone();
        }

        // A fully-neutral material: white base + flat normal + white MR/emissive/AO
        // + default params. Everything sourced from the shared defaults.
        self.ensure_material_defaults(device, queue);
        let d = self
            .material_defaults()
            .expect("material defaults ensured above");
        let bg = Self::assemble_material_bind_group(
            device,
            layout,
            &d.white_view,
            &d.sampler,
            &d.flat_normal_view,
            &d.white_view,
            &d.white_view,
            &d.white_view,
            &d.params_buffer,
            KEY,
        );
        self.texture_cache.insert(KEY.to_string(), bg.clone());
        bg
    }

    /// Return (creating once) a 256×256 grey checkerboard texture.
    ///
    /// Used for geometry whose material has no texture assigned — makes UVs
    /// immediately visible in the editor.
    pub fn create_checkerboard_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
    ) -> Arc<wgpu::BindGroup> {
        const KEY: &str = "__checkerboard_texture__";
        const SIZE: u32 = 256;
        const CELL: u32 = 32; // pixels per checker square

        if let Some(cached) = self.texture_cache.get(KEY) {
            return cached.clone();
        }

        let mut pixels = vec![0u8; (SIZE * SIZE * 4) as usize];
        for y in 0..SIZE {
            for x in 0..SIZE {
                let light = ((x / CELL) + (y / CELL)).is_multiple_of(2);
                let luma = if light { 200u8 } else { 50u8 };
                let base = ((y * SIZE + x) * 4) as usize;
                pixels[base] = luma;
                pixels[base + 1] = luma;
                pixels[base + 2] = luma;
                pixels[base + 3] = 255;
            }
        }

        // SIZE and pixel count are compile-time constants; this cannot fail.

        self.install_decoded_material_texture(device, queue, layout, KEY, &pixels, SIZE, SIZE)
            .expect("checkerboard texture creation must not fail")
    }

    pub fn create_uv_debug_texture(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        layout: &wgpu::BindGroupLayout,
    ) -> Arc<wgpu::BindGroup> {
        const KEY: &str = "__uv_debug_texture__";
        const TEXTURE_SIZE: usize = 8;

        if let Some(cached) = self.texture_cache.get(KEY) {
            return cached.clone();
        }

        let mut palette: [u8; 32] = [
            255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255,
            198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255,
        ];

        let mut texture_data = [0u8; TEXTURE_SIZE * TEXTURE_SIZE * 4];
        for y in 0..TEXTURE_SIZE {
            let offset = TEXTURE_SIZE * y * 4;
            texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
            palette.rotate_right(4);
        }

        self.install_decoded_material_texture(
            device,
            queue,
            layout,
            KEY,
            &texture_data,
            TEXTURE_SIZE as u32,
            TEXTURE_SIZE as u32,
        )
        .expect("uv debug texture creation must not fail")
    }

    // ── Private GPU helpers ───────────────────────────────────────────────

    /// Decode a texture file to RGBA8, preferring embedded data over disk.
    fn decode_texture_rgba(&self, resolved_path: &str) -> Result<(Vec<u8>, u32, u32), AssetError> {
        if let Some(data) = self.embedded_assets.get(resolved_path) {
            let img = image::load_from_memory(data)
                .map_err(|source| AssetError::ImageDecode {
                    path: std::path::PathBuf::from(resolved_path),
                    source,
                })?
                .to_rgba8();
            let (w, h) = img.dimensions();
            return Ok((img.into_raw(), w, h));
        }

        decode_rgba_image_file(resolved_path)
    }
}