nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::asset_id::TextureId;
use crate::ecs::generational_registry::GenerationalRegistry;
use std::collections::HashMap;
use wgpu::util::DeviceExt;

pub struct TextureCache {
    pub registry: GenerationalRegistry<TextureEntry>,
    pub pending_references: HashMap<String, usize>,
}

impl Default for TextureCache {
    fn default() -> Self {
        Self {
            registry: GenerationalRegistry::new(),
            pending_references: HashMap::new(),
        }
    }
}

impl TextureCache {
    pub fn load_texture_from_raw_rgba(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        name: String,
        rgba_data: &[u8],
        dimensions: (u32, u32),
    ) -> Result<TextureId, String> {
        texture_cache_load_from_raw_rgba(self, device, queue, name, rgba_data, dimensions)
    }

    pub fn get(&self, name: &str) -> Option<&TextureEntry> {
        let index = self.registry.name_to_index.get(name)?;
        self.registry.entries[*index as usize].as_ref()
    }
}

pub struct TextureEntry {
    pub texture: wgpu::Texture,
    pub view: wgpu::TextureView,
    pub sampler: wgpu::Sampler,
}

pub fn texture_cache_load_from_raw_rgba(
    cache: &mut TextureCache,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    name: String,
    rgba_data: &[u8],
    dimensions: (u32, u32),
) -> Result<TextureId, String> {
    texture_cache_load_from_raw_rgba_with_format(
        cache,
        device,
        queue,
        name,
        rgba_data,
        dimensions,
        wgpu::TextureFormat::Rgba8Unorm,
    )
}

pub fn texture_cache_load_from_raw_rgba_with_format(
    cache: &mut TextureCache,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    name: String,
    rgba_data: &[u8],
    dimensions: (u32, u32),
    format: wgpu::TextureFormat,
) -> Result<TextureId, String> {
    if let Some((index, generation)) = cache.registry.lookup_index(&name) {
        return Ok(TextureId { index, generation });
    }

    let pending_refs = cache.pending_references.remove(&name).unwrap_or(0);

    let size = wgpu::Extent3d {
        width: dimensions.0,
        height: dimensions.1,
        depth_or_array_layers: 1,
    };

    let texture = device.create_texture_with_data(
        queue,
        &wgpu::TextureDescriptor {
            label: Some(&name),
            size,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        },
        wgpu::util::TextureDataOrder::LayerMajor,
        rgba_data,
    );

    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());

    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
        label: Some(&format!("{} Sampler", name)),
        address_mode_u: wgpu::AddressMode::Repeat,
        address_mode_v: wgpu::AddressMode::Repeat,
        address_mode_w: wgpu::AddressMode::Repeat,
        mag_filter: wgpu::FilterMode::Nearest,
        min_filter: wgpu::FilterMode::Nearest,
        mipmap_filter: wgpu::MipmapFilterMode::Nearest,
        ..Default::default()
    });

    let entry = TextureEntry {
        texture,
        view,
        sampler,
    };

    let (index, generation) = cache.registry.insert(name, entry);
    cache.registry.reference_counts[index as usize] = 1 + pending_refs;

    Ok(TextureId { index, generation })
}

pub fn texture_cache_add_reference(cache: &mut TextureCache, name: &str) {
    if let Some(&index) = cache.registry.name_to_index.get(name) {
        cache.registry.add_reference(index);
    } else {
        *cache
            .pending_references
            .entry(name.to_string())
            .or_insert(0) += 1;
    }
}

pub fn texture_cache_remove_reference(cache: &mut TextureCache, name: &str) {
    if let Some(&index) = cache.registry.name_to_index.get(name) {
        cache.registry.remove_reference(index);
    } else if let Some(count) = cache.pending_references.get_mut(name) {
        *count = count.saturating_sub(1);
        if *count == 0 {
            cache.pending_references.remove(name);
        }
    }
}

pub fn texture_cache_remove_unused(cache: &mut TextureCache) -> Vec<String> {
    cache.registry.remove_unused()
}

pub enum TextureReloadResult {
    UpdatedInPlace,
    Recreated,
    NotFound,
}

pub fn texture_cache_reload(
    cache: &mut TextureCache,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    name: &str,
    rgba_data: &[u8],
    width: u32,
    height: u32,
) -> TextureReloadResult {
    let Some(&index) = cache.registry.name_to_index.get(name) else {
        tracing::warn!("Texture reload: '{}' not found in cache", name);
        return TextureReloadResult::NotFound;
    };
    let Some(entry) = cache.registry.entries[index as usize].as_ref() else {
        tracing::warn!("Texture reload: '{}' entry is empty", name);
        return TextureReloadResult::NotFound;
    };

    let existing_size = entry.texture.size();
    if existing_size.width == width && existing_size.height == height {
        tracing::info!(
            "Texture reload: '{}' updated in-place ({}x{})",
            name,
            width,
            height
        );
        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &entry.texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            rgba_data,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(4 * width),
                rows_per_image: Some(height),
            },
            wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
        );
        TextureReloadResult::UpdatedInPlace
    } else {
        let size = wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        };
        let texture = device.create_texture_with_data(
            queue,
            &wgpu::TextureDescriptor {
                label: Some(name),
                size,
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: wgpu::TextureFormat::Rgba8Unorm,
                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
                view_formats: &[],
            },
            wgpu::util::TextureDataOrder::LayerMajor,
            rgba_data,
        );
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some(&format!("{} Sampler", name)),
            address_mode_u: wgpu::AddressMode::Repeat,
            address_mode_v: wgpu::AddressMode::Repeat,
            address_mode_w: wgpu::AddressMode::Repeat,
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
            ..Default::default()
        });
        if let Some(slot) = cache.registry.entries[index as usize].as_mut() {
            *slot = TextureEntry {
                texture,
                view,
                sampler,
            };
        }
        tracing::info!(
            "Texture reload: '{}' recreated ({}x{} -> {}x{})",
            name,
            existing_size.width,
            existing_size.height,
            width,
            height
        );
        TextureReloadResult::Recreated
    }
}