Skip to main content

oxide_renderer/texture/
mod.rs

1//! Texture loading and management
2
3mod fallback;
4mod sampler;
5
6use std::path::Path;
7
8use wgpu::{Device, Queue};
9
10pub use fallback::FallbackTexture;
11pub use sampler::SamplerDescriptor;
12
13#[derive(thiserror::Error, Debug)]
14pub enum TextureError {
15    #[error("Failed to load image '{path}': {source}")]
16    ImageLoad {
17        path: String,
18        source: image::ImageError,
19    },
20    #[error("Failed to read image file '{path}': {source}")]
21    Io {
22        path: String,
23        source: std::io::Error,
24    },
25    #[error("Invalid texture dimensions: {width}x{height}")]
26    InvalidDimensions { width: u32, height: u32 },
27}
28
29/// A GPU texture with its view and sampler.
30#[derive(Debug)]
31pub struct Texture {
32    pub texture: wgpu::Texture,
33    pub view: wgpu::TextureView,
34    pub sampler: wgpu::Sampler,
35}
36
37impl Texture {
38    /// Creates a texture from raw bytes (RGBA format).
39    pub fn from_bytes(
40        device: &Device,
41        queue: &Queue,
42        bytes: &[u8],
43        dimensions: (u32, u32),
44        label: Option<&str>,
45    ) -> Self {
46        let (width, height) = dimensions;
47
48        let texture = device.create_texture(&wgpu::TextureDescriptor {
49            label,
50            size: wgpu::Extent3d {
51                width,
52                height,
53                depth_or_array_layers: 1,
54            },
55            mip_level_count: 1,
56            sample_count: 1,
57            dimension: wgpu::TextureDimension::D2,
58            format: wgpu::TextureFormat::Rgba8UnormSrgb,
59            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
60            view_formats: &[],
61        });
62
63        queue.write_texture(
64            texture.as_image_copy(),
65            bytes,
66            wgpu::TexelCopyBufferLayout {
67                offset: 0,
68                bytes_per_row: Some(4 * width),
69                rows_per_image: Some(height),
70            },
71            wgpu::Extent3d {
72                width,
73                height,
74                depth_or_array_layers: 1,
75            },
76        );
77
78        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
79
80        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
81            address_mode_u: wgpu::AddressMode::ClampToEdge,
82            address_mode_v: wgpu::AddressMode::ClampToEdge,
83            address_mode_w: wgpu::AddressMode::ClampToEdge,
84            mag_filter: wgpu::FilterMode::Linear,
85            min_filter: wgpu::FilterMode::Linear,
86            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
87            ..Default::default()
88        });
89
90        Self {
91            texture,
92            view,
93            sampler,
94        }
95    }
96
97    /// Loads a texture from a file (PNG or JPEG).
98    pub fn from_file(
99        device: &Device,
100        queue: &Queue,
101        path: impl AsRef<Path>,
102    ) -> Result<Self, TextureError> {
103        let path = path.as_ref();
104        let path_str = path.display().to_string();
105
106        let img = image::open(path).map_err(|source| TextureError::ImageLoad {
107            path: path_str.clone(),
108            source,
109        })?;
110
111        let rgba = img.to_rgba8();
112        let dimensions = rgba.dimensions();
113
114        Ok(Self::from_bytes(
115            device,
116            queue,
117            &rgba,
118            dimensions,
119            Some(&path_str),
120        ))
121    }
122
123    /// Creates a texture with a custom sampler.
124    pub fn with_sampler(mut self, device: &Device, descriptor: &SamplerDescriptor) -> Self {
125        self.sampler = device.create_sampler(&wgpu::SamplerDescriptor {
126            address_mode_u: descriptor.address_mode_u,
127            address_mode_v: descriptor.address_mode_v,
128            address_mode_w: descriptor.address_mode_w,
129            mag_filter: descriptor.mag_filter,
130            min_filter: descriptor.min_filter,
131            mipmap_filter: descriptor.mipmap_filter,
132            ..Default::default()
133        });
134        self
135    }
136}