use crate::error::{RenderError, Result};
use std::collections::HashMap;
use std::collections::hash_map::Entry;
pub use mabda::texture::{
CubemapTexture, copy_texture_to_texture, create_default_sampler, mip_level_count,
validate_dimensions,
};
pub struct Texture {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
}
impl Texture {
pub fn from_bytes(
device: &wgpu::Device,
queue: &wgpu::Queue,
bytes: &[u8],
label: &str,
) -> Result<Self> {
tracing::debug!(label, "loading texture from bytes");
let img = image::load_from_memory(bytes)
.map_err(|e| RenderError::Texture(e.to_string()))?
.to_rgba8();
let (width, height) = img.dimensions();
Self::from_rgba(device, queue, &img, width, height, label)
}
pub fn from_color(
device: &wgpu::Device,
queue: &wgpu::Queue,
color: crate::color::Color,
) -> Result<Self> {
let rgba = [
(color.r * 255.0) as u8,
(color.g * 255.0) as u8,
(color.b * 255.0) as u8,
(color.a * 255.0) as u8,
];
Self::from_rgba(device, queue, &rgba, 1, 1, "solid_color")
}
pub fn white_pixel(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self> {
Self::from_color(device, queue, crate::color::Color::WHITE)
}
pub fn from_rgba(
device: &wgpu::Device,
queue: &wgpu::Queue,
rgba: &[u8],
width: u32,
height: u32,
label: &str,
) -> Result<Self> {
let sampler = create_default_sampler(device);
Self::from_rgba_with_sampler(device, queue, rgba, width, height, label, sampler)
}
pub fn from_rgba_with_sampler(
device: &wgpu::Device,
queue: &wgpu::Queue,
rgba: &[u8],
width: u32,
height: u32,
label: &str,
sampler: wgpu::Sampler,
) -> Result<Self> {
if width == 0 || height == 0 {
return Err(RenderError::Texture("zero-size texture".into()));
}
let expected = (width as usize)
.checked_mul(height as usize)
.and_then(|v| v.checked_mul(4))
.ok_or_else(|| RenderError::Texture("texture dimensions overflow".into()))?;
if rgba.len() != expected {
return Err(RenderError::Texture(format!(
"RGBA buffer size mismatch: expected {width}x{height}x4={expected}, got {}",
rgba.len()
)));
}
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
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,
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),
},
size,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Ok(Self {
texture,
view,
sampler,
})
}
pub fn bind_group(
&self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("texture_bind_group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
})
}
#[cfg(feature = "ranga")]
pub fn from_pixel_buffer(
device: &wgpu::Device,
queue: &wgpu::Queue,
buffer: &ranga::pixel::PixelBuffer,
label: &str,
) -> Result<Self> {
let rgba_data = match buffer.format {
ranga::pixel::PixelFormat::Rgba8 => &buffer.data,
_ => {
return Err(RenderError::Texture(format!(
"Unsupported pixel format {:?}, expected Rgba8",
buffer.format
)));
}
};
Self::from_rgba(device, queue, rgba_data, buffer.width, buffer.height, label)
}
#[must_use]
#[inline]
pub fn size(&self) -> (u32, u32) {
let s = self.texture.size();
(s.width, s.height)
}
}
struct CachedTexture {
texture: Texture,
bind_group: wgpu::BindGroup,
}
pub struct TextureCache {
entries: HashMap<u64, CachedTexture>,
}
impl TextureCache {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn insert(
&mut self,
id: u64,
texture: Texture,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) {
tracing::debug!(id, "texture cache insert");
let bind_group = texture.bind_group(device, layout);
self.entries.insert(
id,
CachedTexture {
texture,
bind_group,
},
);
}
pub fn get_or_load(
&mut self,
id: u64,
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
bytes: &[u8],
label: &str,
) -> Result<&wgpu::BindGroup> {
match self.entries.entry(id) {
Entry::Occupied(e) => {
tracing::debug!(id, "texture cache hit");
Ok(&e.into_mut().bind_group)
}
Entry::Vacant(e) => {
let texture = Texture::from_bytes(device, queue, bytes, label)?;
let bind_group = texture.bind_group(device, layout);
let cached = e.insert(CachedTexture {
texture,
bind_group,
});
Ok(&cached.bind_group)
}
}
}
#[must_use]
pub fn get_bind_group(&self, id: u64) -> Option<&wgpu::BindGroup> {
self.entries.get(&id).map(|e| &e.bind_group)
}
#[must_use]
#[inline]
pub fn contains(&self, id: u64) -> bool {
self.entries.contains_key(&id)
}
#[must_use]
pub fn get(&self, id: u64) -> Option<&Texture> {
self.entries.get(&id).map(|e| &e.texture)
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for TextureCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn texture_cache_empty() {
let cache = TextureCache::new();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(!cache.contains(0));
assert!(cache.get(0).is_none());
assert!(cache.get_bind_group(0).is_none());
}
#[test]
fn texture_cache_default() {
let cache = TextureCache::default();
assert!(cache.is_empty());
}
#[test]
fn mip_level_count_values() {
assert_eq!(mip_level_count(1, 1), 1);
assert_eq!(mip_level_count(2, 2), 2);
assert_eq!(mip_level_count(4, 4), 3);
assert_eq!(mip_level_count(256, 256), 9);
assert_eq!(mip_level_count(1024, 512), 11);
assert_eq!(mip_level_count(1, 512), 10);
assert_eq!(mip_level_count(0, 0), 1);
}
#[test]
fn validate_dimensions_within_limits() {
let limits = wgpu::Limits {
max_texture_dimension_2d: 8192,
..Default::default()
};
assert!(validate_dimensions(1024, 1024, &limits).is_ok());
assert!(validate_dimensions(8192, 8192, &limits).is_ok());
}
#[test]
fn validate_dimensions_exceeds_limits() {
let limits = wgpu::Limits {
max_texture_dimension_2d: 8192,
..Default::default()
};
assert!(validate_dimensions(8193, 1024, &limits).is_err());
assert!(validate_dimensions(1024, 8193, &limits).is_err());
}
}