nightshade 0.14.1

A cross-platform data-oriented game engine.
Documentation
use crate::render::wgpu::texture_cache::{SamplerWrap, TextureUsage};
use std::collections::HashMap;

const DEFAULT_LAYER_SIZE: u32 = 1024;
const DEFAULT_MAX_LAYERS: u32 = 256;

pub const NO_TEXTURE_LAYER: u32 = 0xFFFFu32;

fn wrap_code(wrap: SamplerWrap) -> u32 {
    match wrap {
        SamplerWrap::Repeat => 0,
        SamplerWrap::MirroredRepeat => 1,
        SamplerWrap::ClampToEdge => 2,
    }
}

pub fn pack_layer(layer: u32, wrap_u: SamplerWrap, wrap_v: SamplerWrap) -> u32 {
    (layer & 0xFFFFu32) | (wrap_code(wrap_u) << 16) | (wrap_code(wrap_v) << 18)
}

#[derive(Copy, Clone, Debug)]
pub struct MaterialTextureLayer {
    pub usage: TextureUsage,
    pub layer: u32,
    pub wrap_u: SamplerWrap,
    pub wrap_v: SamplerWrap,
}

impl MaterialTextureLayer {
    pub fn packed(&self) -> u32 {
        pack_layer(self.layer, self.wrap_u, self.wrap_v)
    }
}

pub struct MaterialTextureUpload<'a> {
    pub name: String,
    pub rgba_data: &'a [u8],
    pub width: u32,
    pub height: u32,
    pub usage: TextureUsage,
    pub wrap_u: SamplerWrap,
    pub wrap_v: SamplerWrap,
}

pub const DEFAULT_ANISOTROPY: u16 = 16;

pub struct MaterialTextureArrays {
    pub layer_size: u32,
    pub max_layers: u32,
    pub mip_level_count: u32,
    pub srgb_texture: wgpu::Texture,
    pub linear_texture: wgpu::Texture,
    pub srgb_view: wgpu::TextureView,
    pub linear_view: wgpu::TextureView,
    pub sampler: wgpu::Sampler,
    pub anisotropy_clamp: u16,
    pub srgb_next_layer: u32,
    pub linear_next_layer: u32,
    pub layer_map: HashMap<String, MaterialTextureLayer>,
    pub srgb_free_layers: Vec<u32>,
    pub linear_free_layers: Vec<u32>,
}

fn build_material_sampler(device: &wgpu::Device, anisotropy_clamp: u16) -> wgpu::Sampler {
    let clamped = anisotropy_clamp.clamp(1, 16);
    device.create_sampler(&wgpu::SamplerDescriptor {
        label: Some("Material Texture Array Sampler"),
        address_mode_u: wgpu::AddressMode::ClampToEdge,
        address_mode_v: wgpu::AddressMode::ClampToEdge,
        address_mode_w: wgpu::AddressMode::ClampToEdge,
        mag_filter: wgpu::FilterMode::Linear,
        min_filter: wgpu::FilterMode::Linear,
        mipmap_filter: wgpu::MipmapFilterMode::Linear,
        anisotropy_clamp: clamped,
        ..Default::default()
    })
}

impl MaterialTextureArrays {
    pub fn new(device: &wgpu::Device) -> Self {
        Self::with_size(device, DEFAULT_LAYER_SIZE, DEFAULT_MAX_LAYERS)
    }

    pub fn with_size(device: &wgpu::Device, layer_size: u32, max_layers: u32) -> Self {
        let largest = layer_size.max(1);
        let mip_level_count = (largest as f32).log2().floor() as u32 + 1;

        let extent = wgpu::Extent3d {
            width: layer_size,
            height: layer_size,
            depth_or_array_layers: max_layers,
        };

        let srgb_texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Material Texture Array (sRGB)"),
            size: extent,
            mip_level_count,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING
                | wgpu::TextureUsages::COPY_DST
                | wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        let linear_texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Material Texture Array (Linear)"),
            size: extent,
            mip_level_count,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING
                | wgpu::TextureUsages::COPY_DST
                | wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });

        let srgb_view = srgb_texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        });
        let linear_view = linear_texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        });

        let anisotropy_clamp = DEFAULT_ANISOTROPY;
        let sampler = build_material_sampler(device, anisotropy_clamp);

        Self {
            layer_size,
            max_layers,
            mip_level_count,
            srgb_texture,
            linear_texture,
            srgb_view,
            linear_view,
            sampler,
            anisotropy_clamp,
            srgb_next_layer: 0,
            linear_next_layer: 0,
            layer_map: HashMap::new(),
            srgb_free_layers: Vec::new(),
            linear_free_layers: Vec::new(),
        }
    }

    /// Returns a layer slot to the free list so the next upload can reuse
    /// it. Called after the texture cache evicts a no-longer-referenced
    /// texture so loading additional models can scavenge its array slot.
    pub fn release(&mut self, name: &str) -> Option<MaterialTextureLayer> {
        let layer_info = self.layer_map.remove(name)?;
        match layer_info.usage {
            TextureUsage::Color => self.srgb_free_layers.push(layer_info.layer),
            TextureUsage::Linear => self.linear_free_layers.push(layer_info.layer),
        }
        Some(layer_info)
    }

    pub fn upload(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        mip_generator: &super::mip_generator::MipGenerator,
        request: MaterialTextureUpload<'_>,
    ) -> Option<u32> {
        let MaterialTextureUpload {
            name,
            rgba_data,
            width,
            height,
            usage,
            wrap_u,
            wrap_v,
        } = request;
        if let Some(existing) = self.layer_map.get(&name) {
            return Some(existing.layer);
        }

        let layer = match usage {
            TextureUsage::Color => {
                if let Some(reused) = self.srgb_free_layers.pop() {
                    reused
                } else if self.srgb_next_layer >= self.max_layers {
                    tracing::error!("Material sRGB texture array exhausted");
                    return None;
                } else {
                    let layer = self.srgb_next_layer;
                    self.srgb_next_layer += 1;
                    layer
                }
            }
            TextureUsage::Linear => {
                if let Some(reused) = self.linear_free_layers.pop() {
                    reused
                } else if self.linear_next_layer >= self.max_layers {
                    tracing::error!("Material linear texture array exhausted");
                    return None;
                } else {
                    let layer = self.linear_next_layer;
                    self.linear_next_layer += 1;
                    layer
                }
            }
        };

        let resampled = resample_to_size(rgba_data, width, height, self.layer_size);
        let texture = match usage {
            TextureUsage::Color => &self.srgb_texture,
            TextureUsage::Linear => &self.linear_texture,
        };

        queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture,
                mip_level: 0,
                origin: wgpu::Origin3d {
                    x: 0,
                    y: 0,
                    z: layer,
                },
                aspect: wgpu::TextureAspect::All,
            },
            &resampled,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(4 * self.layer_size),
                rows_per_image: Some(self.layer_size),
            },
            wgpu::Extent3d {
                width: self.layer_size,
                height: self.layer_size,
                depth_or_array_layers: 1,
            },
        );

        if self.mip_level_count > 1 {
            let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Material Texture Array Mip Gen"),
            });
            mip_generator.generate_mips(device, &mut encoder, texture, layer);
            queue.submit(std::iter::once(encoder.finish()));
        }

        self.layer_map.insert(
            name,
            MaterialTextureLayer {
                usage,
                layer,
                wrap_u,
                wrap_v,
            },
        );
        Some(layer)
    }

    pub fn get_layer(&self, name: &str) -> Option<MaterialTextureLayer> {
        self.layer_map.get(name).copied()
    }

    pub fn srgb_view(&self) -> &wgpu::TextureView {
        &self.srgb_view
    }

    pub fn linear_view(&self) -> &wgpu::TextureView {
        &self.linear_view
    }

    pub fn sampler(&self) -> &wgpu::Sampler {
        &self.sampler
    }

    /// Replaces the sampler with one using the requested anisotropy clamp.
    /// Returns true when the clamp changed and the sampler was rebuilt.
    /// Callers must update any bind groups that reference the old sampler.
    pub fn set_anisotropy(&mut self, device: &wgpu::Device, anisotropy_clamp: u16) -> bool {
        let clamped = anisotropy_clamp.clamp(1, 16);
        if clamped == self.anisotropy_clamp {
            return false;
        }
        self.sampler = build_material_sampler(device, clamped);
        self.anisotropy_clamp = clamped;
        true
    }
}

fn resample_to_size(rgba_data: &[u8], width: u32, height: u32, target: u32) -> Vec<u8> {
    if width == target && height == target {
        return rgba_data.to_vec();
    }
    let mut out = vec![0u8; (target * target * 4) as usize];
    for y in 0..target {
        let sy = ((y as u64) * (height as u64) / (target as u64)) as u32;
        for x in 0..target {
            let sx = ((x as u64) * (width as u64) / (target as u64)) as u32;
            let src_offset = ((sy * width + sx) * 4) as usize;
            let dst_offset = ((y * target + x) * 4) as usize;
            out[dst_offset..dst_offset + 4].copy_from_slice(&rgba_data[src_offset..src_offset + 4]);
        }
    }
    out
}