1use std::io::Cursor;
2
3use image::{ImageError, ImageReader, Limits};
4
5use crate::Error;
6use crate::assets::Unresolved;
7use crate::gpu::{sampled, sampler};
8use crate::math::UVec2;
9
10const MAX_SIZE: u32 = 8192;
13
14#[derive(Clone, Debug, Default, PartialEq)]
17pub struct TextureData {
18 size: UVec2,
19 pixels: Vec<u8>,
20 pixelated: bool,
21 unresolved: Unresolved,
23}
24
25impl TextureData {
26 pub fn rgba8(size: UVec2, pixels: Vec<u8>) -> Self {
30 debug_assert_eq!(
31 pixels.len() as u64,
32 4 * u64::from(size.x) * u64::from(size.y),
33 "a {}x{} texture needs four bytes per pixel",
34 size.x,
35 size.y
36 );
37
38 Self {
39 size,
40 pixels,
41 pixelated: false,
42 unresolved: Unresolved::default(),
43 }
44 }
45
46 #[must_use]
49 pub fn pixelated(mut self) -> Self {
50 self.pixelated = true;
51 self
52 }
53
54 pub fn size(&self) -> UVec2 {
56 self.size
57 }
58
59 pub(crate) fn drawn(&self) -> bool {
61 self.size.x > 0 && self.size.y > 0
62 }
63
64 pub fn pixels(&self) -> &[u8] {
66 &self.pixels
67 }
68
69 pub(crate) fn missing(unresolved: Unresolved) -> Self {
72 Self {
73 unresolved,
74 ..Self::default()
75 }
76 }
77
78 pub(crate) fn take_unresolved(&mut self) -> Unresolved {
80 self.unresolved.taken()
81 }
82}
83
84#[derive(Clone, Debug, Default, PartialEq)]
91pub struct ReliefData {
92 map: TextureData,
93 deep: bool,
94}
95
96impl ReliefData {
97 pub fn rgba8(size: UVec2, pixels: Vec<u8>) -> Self {
102 Self::held(TextureData::rgba8(size, pixels), true)
103 }
104
105 pub fn normals(size: UVec2, pixels: Vec<u8>) -> Self {
111 Self::held(TextureData::rgba8(size, pixels), false)
112 }
113
114 pub fn size(&self) -> UVec2 {
116 self.map.size()
117 }
118
119 pub(crate) fn loaded(texture: TextureData) -> Self {
121 Self::held(texture, true)
122 }
123
124 pub(crate) fn deep(&self) -> bool {
127 self.deep
128 }
129
130 pub(crate) fn map(&self) -> &TextureData {
133 &self.map
134 }
135
136 pub(crate) fn map_mut(&mut self) -> &mut TextureData {
138 &mut self.map
139 }
140
141 const fn held(map: TextureData, deep: bool) -> Self {
142 Self { map, deep }
143 }
144}
145
146#[derive(Clone, Debug, PartialEq)]
153pub struct ShadingData(TextureData);
154
155impl ShadingData {
156 pub fn rgba8(size: UVec2, pixels: Vec<u8>) -> Self {
160 Self(TextureData::rgba8(size, pixels))
161 }
162
163 pub(crate) fn map(&self) -> &TextureData {
166 &self.0
167 }
168
169 pub(crate) fn map_mut(&mut self) -> &mut TextureData {
171 &mut self.0
172 }
173}
174
175pub(crate) struct Textures {
180 layout: wgpu::BindGroupLayout,
181 blending: wgpu::Sampler,
182 nearest: wgpu::Sampler,
183 white: wgpu::Texture,
184 flat: wgpu::Texture,
185 fallback: wgpu::BindGroup,
186}
187
188impl Textures {
189 pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
190 let map = |binding| sampled(binding, true);
191 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
192 label: Some("mirage-engine slot texture"),
193 entries: &[map(0), sampler(1), map(2), map(3), map(4)],
194 });
195 let blending = repeating_sampler(device, wgpu::FilterMode::Linear);
196 let nearest = repeating_sampler(device, wgpu::FilterMode::Nearest);
197
198 let white = uploaded(
199 device,
200 queue,
201 &TextureData::rgba8(UVec2::ONE, vec![u8::MAX; 4]),
202 wgpu::TextureFormat::Rgba8UnormSrgb,
203 );
204 let flat = uploaded(
207 device,
208 queue,
209 &TextureData::rgba8(UVec2::ONE, vec![128, 128, u8::MAX, 0]),
210 wgpu::TextureFormat::Rgba8Unorm,
211 );
212 let fallback = bindings(device, &layout, &blending, [&white, &flat, &white, &white]);
213 Self {
214 layout,
215 blending,
216 nearest,
217 white,
218 flat,
219 fallback,
220 }
221 }
222
223 pub(crate) fn layout(&self) -> &wgpu::BindGroupLayout {
224 &self.layout
225 }
226
227 pub(crate) fn fallback(&self) -> &wgpu::BindGroup {
230 &self.fallback
231 }
232
233 pub(crate) fn bind(
240 &self,
241 device: &wgpu::Device,
242 queue: &wgpu::Queue,
243 color: Option<&TextureData>,
244 relief: Option<&ReliefData>,
245 shading: Option<&ShadingData>,
246 emissive: Option<&TextureData>,
247 ) -> Option<wgpu::BindGroup> {
248 let color = color.filter(|data| data.drawn());
249 let relief = relief.map(ReliefData::map).filter(|data| data.drawn());
250 let shading = shading.map(ShadingData::map).filter(|data| data.drawn());
251 let emissive = emissive.filter(|data| data.drawn());
252 if [color, relief, shading, emissive]
253 .iter()
254 .all(Option::is_none)
255 {
256 return None;
257 }
258 let sampler = match color.is_some_and(|data| data.pixelated) {
259 true => &self.nearest,
260 false => &self.blending,
261 };
262 let paint =
263 |data: &TextureData| uploaded(device, queue, data, wgpu::TextureFormat::Rgba8UnormSrgb);
264 let raw =
265 |data: &TextureData| uploaded(device, queue, data, wgpu::TextureFormat::Rgba8Unorm);
266 let (base, raised) = (color.map(paint), relief.map(raw));
267 let (scaled, cast) = (shading.map(raw), emissive.map(paint));
268
269 Some(bindings(
270 device,
271 &self.layout,
272 sampler,
273 [
274 base.as_ref().unwrap_or(&self.white),
275 raised.as_ref().unwrap_or(&self.flat),
276 scaled.as_ref().unwrap_or(&self.white),
277 cast.as_ref().unwrap_or(&self.white),
278 ],
279 ))
280 }
281}
282
283pub(crate) fn decode(bytes: &[u8]) -> Result<TextureData, Error> {
288 let mut reader = ImageReader::new(Cursor::new(bytes))
289 .with_guessed_format()
290 .map_err(|error| Error::msg(format!("did not decode: {error}")))?;
291 reader.limits(bounded());
292
293 let decoded = reader.decode().map_err(refused)?.into_rgba8();
294 let size = UVec2::new(decoded.width(), decoded.height());
295
296 Ok(TextureData::rgba8(size, decoded.into_raw()))
297}
298
299fn bounded() -> Limits {
301 let mut limits = Limits::default();
302 limits.max_image_width = Some(MAX_SIZE);
303 limits.max_image_height = Some(MAX_SIZE);
304
305 limits
306}
307
308fn refused(error: ImageError) -> Error {
311 match error {
312 ImageError::Limits(_) => Error::msg(format!(
313 "is larger than the {MAX_SIZE} pixels a side Mirage draws"
314 )),
315 error => Error::msg(format!("did not decode: {error}")),
316 }
317}
318
319fn repeating_sampler(device: &wgpu::Device, filter: wgpu::FilterMode) -> wgpu::Sampler {
322 device.create_sampler(&wgpu::SamplerDescriptor {
323 label: Some("mirage-engine slot texture"),
324 address_mode_u: wgpu::AddressMode::Repeat,
325 address_mode_v: wgpu::AddressMode::Repeat,
326 address_mode_w: wgpu::AddressMode::Repeat,
327 mag_filter: filter,
328 min_filter: filter,
329 ..Default::default()
330 })
331}
332
333fn bindings(
336 device: &wgpu::Device,
337 layout: &wgpu::BindGroupLayout,
338 sampler: &wgpu::Sampler,
339 maps: [&wgpu::Texture; 4],
340) -> wgpu::BindGroup {
341 let [color, relief, shading, emissive] = maps.map(|map| map.create_view(&Default::default()));
342 let map = |binding, view| wgpu::BindGroupEntry {
343 binding,
344 resource: wgpu::BindingResource::TextureView(view),
345 };
346 device.create_bind_group(&wgpu::BindGroupDescriptor {
347 label: Some("mirage-engine slot texture"),
348 layout,
349 entries: &[
350 map(0, &color),
351 wgpu::BindGroupEntry {
352 binding: 1,
353 resource: wgpu::BindingResource::Sampler(sampler),
354 },
355 map(2, &relief),
356 map(3, &shading),
357 map(4, &emissive),
358 ],
359 })
360}
361
362fn uploaded(
364 device: &wgpu::Device,
365 queue: &wgpu::Queue,
366 data: &TextureData,
367 format: wgpu::TextureFormat,
368) -> wgpu::Texture {
369 let extent = wgpu::Extent3d {
370 width: data.size().x,
371 height: data.size().y,
372 depth_or_array_layers: 1,
373 };
374 let texture = device.create_texture(&wgpu::TextureDescriptor {
375 label: Some("mirage-engine slot texture"),
376 size: extent,
377 mip_level_count: 1,
378 sample_count: 1,
379 dimension: wgpu::TextureDimension::D2,
380 format,
381 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
382 view_formats: &[],
383 });
384 queue.write_texture(
385 wgpu::TexelCopyTextureInfo {
386 texture: &texture,
387 mip_level: 0,
388 origin: wgpu::Origin3d::ZERO,
389 aspect: wgpu::TextureAspect::All,
390 },
391 data.pixels(),
392 wgpu::TexelCopyBufferLayout {
393 offset: 0,
394 bytes_per_row: Some(4 * data.size().x),
395 rows_per_image: Some(data.size().y),
396 },
397 extent,
398 );
399
400 texture
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::assets::IMP;
407
408 fn png(width: u32, height: u32) -> Vec<u8> {
410 let mut out = Vec::new();
411 image::RgbaImage::new(width, height)
412 .write_to(&mut Cursor::new(&mut out), image::ImageFormat::Png)
413 .expect("the fixture encodes");
414
415 out
416 }
417
418 #[test]
419 fn a_source_larger_than_a_target_binds_does_not_decode() {
420 let error = decode(&png(MAX_SIZE + 1, 1)).expect_err("no target binds that");
421
422 assert_eq!(
423 error.to_string(),
424 format!("is larger than the {MAX_SIZE} pixels a side Mirage draws")
425 );
426 assert_eq!(
427 decode(&png(MAX_SIZE, 1))
428 .expect("the cap itself is drawn")
429 .size(),
430 UVec2::new(MAX_SIZE, 1),
431 );
432 }
433
434 #[test]
435 fn a_source_cut_off_anywhere_reads_as_itself_or_as_an_error() {
436 let whole = decode(IMP).expect("the fixture decodes").size();
437
438 for at in 0..IMP.len() {
439 if let Ok(cut) = decode(&IMP[..at]) {
440 assert_eq!(cut.size(), whole, "a cut at {at}");
441 }
442 }
443 }
444}