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