Skip to main content

wisp/
texture.rs

1//! Texture types — `Texture` (image), `VideoTexture` (per-frame upload), `RenderTexture` (target).
2
3pub mod render_texture;
4pub mod video_texture;
5
6use std::sync::Arc;
7
8use image::DynamicImage;
9
10use crate::application::Application;
11
12/// GPU image texture backed by a `wgpu::Texture`.
13///
14/// Cheaply cloneable; the underlying GPU resource is `Arc`-wrapped.
15#[derive(Clone)]
16pub struct Texture {
17    inner: Arc<TextureInner>,
18}
19
20impl std::fmt::Debug for Texture {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("Texture")
23            .field("width", &self.inner.width)
24            .field("height", &self.inner.height)
25            .field("id", &self.id())
26            .finish()
27    }
28}
29
30struct TextureInner {
31    texture: wgpu::Texture,
32    view: wgpu::TextureView,
33    sampler: wgpu::Sampler,
34    width: u32,
35    height: u32,
36}
37
38impl Texture {
39    /// Construct from raw RGBA8 bytes in linear-srgb space.
40    ///
41    /// `bytes.len()` must equal `width * height * 4`.
42    ///
43    /// # Panics
44    ///
45    /// Panics if the byte slice length doesn't match `width * height * 4`.
46    #[track_caller]
47    #[must_use]
48    pub fn from_rgba(app: &Application, width: u32, height: u32, bytes: &[u8]) -> Self {
49        let expected = (width as usize)
50            .checked_mul(height as usize)
51            .and_then(|n| n.checked_mul(4))
52            .expect("Texture::from_rgba: dimensions overflow usize");
53        assert_eq!(
54            bytes.len(),
55            expected,
56            "Texture::from_rgba: byte length mismatch (got {got}, expected {expected} for {width}×{height} RGBA)",
57            got = bytes.len(),
58        );
59
60        let device = app.device();
61        let queue = app.queue();
62        let texture = device.create_texture(&wgpu::TextureDescriptor {
63            label: Some("wisp::Texture"),
64            size: wgpu::Extent3d {
65                width,
66                height,
67                depth_or_array_layers: 1,
68            },
69            mip_level_count: 1,
70            sample_count: 1,
71            dimension: wgpu::TextureDimension::D2,
72            format: wgpu::TextureFormat::Rgba8UnormSrgb,
73            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
74            view_formats: &[],
75        });
76
77        queue.write_texture(
78            wgpu::TexelCopyTextureInfo {
79                texture: &texture,
80                mip_level: 0,
81                origin: wgpu::Origin3d::ZERO,
82                aspect: wgpu::TextureAspect::All,
83            },
84            bytes,
85            wgpu::TexelCopyBufferLayout {
86                offset: 0,
87                bytes_per_row: Some(width * 4),
88                rows_per_image: Some(height),
89            },
90            wgpu::Extent3d {
91                width,
92                height,
93                depth_or_array_layers: 1,
94            },
95        );
96
97        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
98        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
99            label: Some("wisp::Texture sampler"),
100            mag_filter: wgpu::FilterMode::Linear,
101            min_filter: wgpu::FilterMode::Linear,
102            ..Default::default()
103        });
104
105        Self {
106            inner: Arc::new(TextureInner {
107                texture,
108                view,
109                sampler,
110                width,
111                height,
112            }),
113        }
114    }
115
116    /// Construct from a decoded image. Converts to RGBA8 if needed.
117    #[must_use]
118    pub fn from_image(app: &Application, img: &DynamicImage) -> Self {
119        let rgba = img.to_rgba8();
120        Self::from_rgba(app, rgba.width(), rgba.height(), rgba.as_raw())
121    }
122
123    /// Wrap a set of wgpu texture handles as a [`Texture`].
124    ///
125    /// Used internally by
126    /// [`crate::texture::render_texture::RenderTexture::as_texture`]
127    /// to expose a render-target as a sampled sprite texture without
128    /// copying GPU bytes. Not part of the public API — render textures
129    /// are the supported caller, and `pub(crate)` keeps it that way.
130    pub(crate) fn from_render_texture_parts(
131        texture: wgpu::Texture,
132        view: wgpu::TextureView,
133        sampler: wgpu::Sampler,
134        width: u32,
135        height: u32,
136    ) -> Self {
137        Self {
138            inner: Arc::new(TextureInner {
139                texture,
140                view,
141                sampler,
142                width,
143                height,
144            }),
145        }
146    }
147
148    /// Construct an empty (zeroed) texture for the given format.
149    ///
150    /// Usage flags include `TEXTURE_BINDING | COPY_DST`, suitable for
151    /// sampling and per-frame uploads (e.g. backing a [`crate::texture::video_texture`]).
152    /// For render targets see [`crate::texture::render_texture`].
153    #[must_use]
154    pub fn empty(app: &Application, width: u32, height: u32, format: wgpu::TextureFormat) -> Self {
155        let device = app.device();
156        let texture = device.create_texture(&wgpu::TextureDescriptor {
157            label: Some("wisp::Texture::empty"),
158            size: wgpu::Extent3d {
159                width,
160                height,
161                depth_or_array_layers: 1,
162            },
163            mip_level_count: 1,
164            sample_count: 1,
165            dimension: wgpu::TextureDimension::D2,
166            format,
167            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
168            view_formats: &[],
169        });
170        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
171        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
172            label: Some("wisp::Texture::empty sampler"),
173            mag_filter: wgpu::FilterMode::Linear,
174            min_filter: wgpu::FilterMode::Linear,
175            ..Default::default()
176        });
177        Self {
178            inner: Arc::new(TextureInner {
179                texture,
180                view,
181                sampler,
182                width,
183                height,
184            }),
185        }
186    }
187
188    /// Texture width in pixels.
189    #[must_use]
190    pub fn width(&self) -> u32 {
191        self.inner.width
192    }
193
194    /// Texture height in pixels.
195    #[must_use]
196    pub fn height(&self) -> u32 {
197        self.inner.height
198    }
199
200    pub(crate) fn view(&self) -> &wgpu::TextureView {
201        &self.inner.view
202    }
203
204    pub(crate) fn sampler(&self) -> &wgpu::Sampler {
205        &self.inner.sampler
206    }
207
208    pub(crate) fn wgpu_texture(&self) -> &wgpu::Texture {
209        &self.inner.texture
210    }
211
212    /// Identity for batching — texture-pointer-equality.
213    ///
214    /// Two `Texture` clones (or two views into the same `Arc`) share the same
215    /// id; two independently-constructed textures don't. Used by the renderer
216    /// to group sprite draws.
217    pub(crate) fn id(&self) -> usize {
218        Arc::as_ptr(&self.inner) as usize
219    }
220}