1use std::collections::HashMap;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5
6use super::gpu::GpuContext;
7
8pub type TextureId = u32;
10
11struct TextureEntry {
13 _texture: wgpu::Texture,
14 bind_group: wgpu::BindGroup,
15 width: u32,
16 height: u32,
17}
18
19pub 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, }
33 }
34
35 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 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 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 pub fn get_bind_group(&self, id: TextureId) -> Option<&wgpu::BindGroup> {
305 self.textures.get(&id).map(|e| &e.bind_group)
306 }
307
308 pub fn get_dimensions(&self, id: TextureId) -> Option<(u32, u32)> {
310 self.textures.get(&id).map(|e| (e.width, e.height))
311 }
312}