Skip to main content

arcane_engine/renderer/
texture.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5
6use super::gpu::GpuContext;
7
8/// Opaque handle to a loaded texture.
9pub type TextureId = u32;
10
11/// Entry for a single loaded texture.
12struct TextureEntry {
13    _texture: wgpu::Texture,
14    bind_group: wgpu::BindGroup,
15    width: u32,
16    height: u32,
17}
18
19/// Handle-based texture store. Loads PNGs, uploads to GPU, returns opaque handles.
20pub struct TextureStore {
21    textures: HashMap<TextureId, TextureEntry>,
22    path_to_id: HashMap<String, TextureId>,
23    next_id: TextureId,
24}
25
26impl TextureStore {
27    pub fn new() -> Self {
28        Self {
29            textures: HashMap::new(),
30            path_to_id: HashMap::new(),
31            next_id: 1, // 0 reserved for "no texture"
32        }
33    }
34
35    /// Load a texture from a PNG file. Returns the texture handle.
36    /// If the same path was already loaded, returns the cached handle.
37    pub fn load(
38        &mut self,
39        gpu: &GpuContext,
40        bind_group_layout: &wgpu::BindGroupLayout,
41        path: &Path,
42    ) -> Result<TextureId> {
43        let path_str = path.to_string_lossy().to_string();
44
45        if let Some(&id) = self.path_to_id.get(&path_str) {
46            return Ok(id);
47        }
48
49        let img_data = std::fs::read(path)
50            .with_context(|| format!("Failed to read texture: {}", path.display()))?;
51
52        let img = image::load_from_memory(&img_data)
53            .with_context(|| format!("Failed to decode image: {}", path.display()))?
54            .to_rgba8();
55
56        let (width, height) = img.dimensions();
57
58        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
59            label: Some(&path_str),
60            size: wgpu::Extent3d {
61                width,
62                height,
63                depth_or_array_layers: 1,
64            },
65            mip_level_count: 1,
66            sample_count: 1,
67            dimension: wgpu::TextureDimension::D2,
68            format: wgpu::TextureFormat::Rgba8UnormSrgb,
69            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
70            view_formats: &[],
71        });
72
73        gpu.queue.write_texture(
74            wgpu::TexelCopyTextureInfo {
75                texture: &texture,
76                mip_level: 0,
77                origin: wgpu::Origin3d::ZERO,
78                aspect: wgpu::TextureAspect::All,
79            },
80            &img,
81            wgpu::TexelCopyBufferLayout {
82                offset: 0,
83                bytes_per_row: Some(4 * width),
84                rows_per_image: Some(height),
85            },
86            wgpu::Extent3d {
87                width,
88                height,
89                depth_or_array_layers: 1,
90            },
91        );
92
93        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
94        let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
95            address_mode_u: wgpu::AddressMode::ClampToEdge,
96            address_mode_v: wgpu::AddressMode::ClampToEdge,
97            mag_filter: wgpu::FilterMode::Nearest,
98            min_filter: wgpu::FilterMode::Nearest,
99            ..Default::default()
100        });
101
102        let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
103            label: Some(&format!("texture_bind_group_{}", self.next_id)),
104            layout: bind_group_layout,
105            entries: &[
106                wgpu::BindGroupEntry {
107                    binding: 0,
108                    resource: wgpu::BindingResource::TextureView(&view),
109                },
110                wgpu::BindGroupEntry {
111                    binding: 1,
112                    resource: wgpu::BindingResource::Sampler(&sampler),
113                },
114            ],
115        });
116
117        let id = self.next_id;
118        self.next_id += 1;
119
120        self.textures.insert(
121            id,
122            TextureEntry {
123                _texture: texture,
124                bind_group,
125                width,
126                height,
127            },
128        );
129        self.path_to_id.insert(path_str, id);
130
131        Ok(id)
132    }
133
134    /// Create a solid-color 1x1 texture. Useful for placeholder sprites.
135    pub fn create_solid_color(
136        &mut self,
137        gpu: &GpuContext,
138        bind_group_layout: &wgpu::BindGroupLayout,
139        name: &str,
140        r: u8,
141        g: u8,
142        b: u8,
143        a: u8,
144    ) -> TextureId {
145        let path_key = format!("__solid__{name}");
146        if let Some(&id) = self.path_to_id.get(&path_key) {
147            return id;
148        }
149
150        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
151            label: Some(name),
152            size: wgpu::Extent3d {
153                width: 1,
154                height: 1,
155                depth_or_array_layers: 1,
156            },
157            mip_level_count: 1,
158            sample_count: 1,
159            dimension: wgpu::TextureDimension::D2,
160            format: wgpu::TextureFormat::Rgba8UnormSrgb,
161            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
162            view_formats: &[],
163        });
164
165        gpu.queue.write_texture(
166            wgpu::TexelCopyTextureInfo {
167                texture: &texture,
168                mip_level: 0,
169                origin: wgpu::Origin3d::ZERO,
170                aspect: wgpu::TextureAspect::All,
171            },
172            &[r, g, b, a],
173            wgpu::TexelCopyBufferLayout {
174                offset: 0,
175                bytes_per_row: Some(4),
176                rows_per_image: Some(1),
177            },
178            wgpu::Extent3d {
179                width: 1,
180                height: 1,
181                depth_or_array_layers: 1,
182            },
183        );
184
185        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
186        let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
187            mag_filter: wgpu::FilterMode::Nearest,
188            min_filter: wgpu::FilterMode::Nearest,
189            ..Default::default()
190        });
191
192        let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
193            label: Some(&format!("solid_color_bind_group_{name}")),
194            layout: bind_group_layout,
195            entries: &[
196                wgpu::BindGroupEntry {
197                    binding: 0,
198                    resource: wgpu::BindingResource::TextureView(&view),
199                },
200                wgpu::BindGroupEntry {
201                    binding: 1,
202                    resource: wgpu::BindingResource::Sampler(&sampler),
203                },
204            ],
205        });
206
207        let id = self.next_id;
208        self.next_id += 1;
209
210        self.textures.insert(
211            id,
212            TextureEntry {
213                _texture: texture,
214                bind_group,
215                width: 1,
216                height: 1,
217            },
218        );
219        self.path_to_id.insert(path_key, id);
220
221        id
222    }
223
224    /// Upload raw RGBA pixel data as a texture with a pre-assigned ID.
225    /// Used for procedurally generated textures (e.g., built-in font).
226    pub fn upload_raw(
227        &mut self,
228        gpu: &GpuContext,
229        bind_group_layout: &wgpu::BindGroupLayout,
230        id: TextureId,
231        pixels: &[u8],
232        width: u32,
233        height: u32,
234    ) {
235        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
236            label: Some(&format!("raw_texture_{id}")),
237            size: wgpu::Extent3d {
238                width,
239                height,
240                depth_or_array_layers: 1,
241            },
242            mip_level_count: 1,
243            sample_count: 1,
244            dimension: wgpu::TextureDimension::D2,
245            format: wgpu::TextureFormat::Rgba8UnormSrgb,
246            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
247            view_formats: &[],
248        });
249
250        gpu.queue.write_texture(
251            wgpu::TexelCopyTextureInfo {
252                texture: &texture,
253                mip_level: 0,
254                origin: wgpu::Origin3d::ZERO,
255                aspect: wgpu::TextureAspect::All,
256            },
257            pixels,
258            wgpu::TexelCopyBufferLayout {
259                offset: 0,
260                bytes_per_row: Some(4 * width),
261                rows_per_image: Some(height),
262            },
263            wgpu::Extent3d {
264                width,
265                height,
266                depth_or_array_layers: 1,
267            },
268        );
269
270        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
271        let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
272            mag_filter: wgpu::FilterMode::Nearest,
273            min_filter: wgpu::FilterMode::Nearest,
274            ..Default::default()
275        });
276
277        let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
278            label: Some(&format!("raw_texture_bind_group_{id}")),
279            layout: bind_group_layout,
280            entries: &[
281                wgpu::BindGroupEntry {
282                    binding: 0,
283                    resource: wgpu::BindingResource::TextureView(&view),
284                },
285                wgpu::BindGroupEntry {
286                    binding: 1,
287                    resource: wgpu::BindingResource::Sampler(&sampler),
288                },
289            ],
290        });
291
292        self.textures.insert(
293            id,
294            TextureEntry {
295                _texture: texture,
296                bind_group,
297                width,
298                height,
299            },
300        );
301    }
302
303    /// Get the bind group for a texture handle.
304    pub fn get_bind_group(&self, id: TextureId) -> Option<&wgpu::BindGroup> {
305        self.textures.get(&id).map(|e| &e.bind_group)
306    }
307
308    /// Get texture dimensions.
309    pub fn get_dimensions(&self, id: TextureId) -> Option<(u32, u32)> {
310        self.textures.get(&id).map(|e| (e.width, e.height))
311    }
312}