1use crate::{
2 assets::{handle::Handle, storage::Assets, upload::Asset},
3 ecs::system::Res,
4 wgpu::{backend::WGPUBackend, gpu_context::GpuContext, mipmap::MipmapGenerator, texture_format::TextureFormat},
5};
6
7pub struct Texture {
11 file: Option<&'static str>,
14 width: u32,
16 height: u32,
18 format: TextureFormat,
20 data: Option<Vec<u8>>,
22 generate_mips: bool,
24}
25
26pub struct TextureBuilder {
32 file: Option<&'static str>,
33 width: u32,
34 height: u32,
35 format: TextureFormat,
36 data: Option<Vec<u8>>,
37 generate_mips: bool,
38}
39
40impl TextureBuilder {
41 pub fn from_file(path: &'static str) -> Self {
44 Self {
45 file: Some(path),
46 width: 0,
47 height: 0,
48 format: TextureFormat::Rgba8UnormSrgb,
49 data: None,
50 generate_mips: false,
51 }
52 }
53
54 pub fn from_data(width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
56 Self {
57 file: None,
58 width,
59 height,
60 format,
61 data: Some(data),
62 generate_mips: false,
63 }
64 }
65
66 pub fn empty(width: u32, height: u32, format: TextureFormat) -> Self {
69 Self {
70 file: None,
71 width,
72 height,
73 format,
74 data: None,
75 generate_mips: false,
76 }
77 }
78
79 pub fn with_format(mut self, format: TextureFormat) -> Self {
80 self.format = format;
81 self
82 }
83
84 pub fn with_mips(mut self) -> Self {
85 self.generate_mips = true;
86 self
87 }
88
89 fn validate(&self) {
93 if self.data.is_some() && (self.width == 0 || self.height == 0) {
94 tracing::warn!(
95 "TextureBuilder::from_data(): width/height is 0 ({}x{}) — did you swap the \
96 argument order, or forget to pass the real dimensions?",
97 self.width,
98 self.height,
99 );
100 }
101 }
102
103 pub fn build(self) -> Texture {
105 self.validate();
106 Texture {
107 file: self.file,
108 width: self.width,
109 height: self.height,
110 format: self.format,
111 data: self.data,
112 generate_mips: self.generate_mips,
113 }
114 }
115
116 pub fn build_asset(self, name: &str, assets: &mut Assets<Texture>) -> Handle<Texture> {
119 let texture = self.build();
120 assets.insert(name, texture)
121 }
122}
123
124pub struct GPUTexture {
131 texture: wgpu::Texture,
132 view: wgpu::TextureView,
133 width: u32,
134 height: u32,
135 format: TextureFormat,
136 ctx: GpuContext,
137}
138
139impl GPUTexture {
140 pub fn write(&self, pixels: &[u8]) {
145 write_texture_level0(self.ctx.queue(), &self.texture, 0, self.format.into(), self.width, self.height, pixels);
146 }
147
148 pub fn width(&self) -> u32 {
149 self.width
150 }
151
152 pub fn height(&self) -> u32 {
153 self.height
154 }
155
156 pub(crate) fn view(&self) -> &wgpu::TextureView {
157 &self.view
158 }
159}
160
161pub(crate) fn write_texture_level0(
168 queue: &wgpu::Queue,
169 texture: &wgpu::Texture,
170 origin_z: u32,
171 format: wgpu::TextureFormat,
172 width: u32,
173 height: u32,
174 pixels: &[u8],
175) {
176 queue.write_texture(
177 wgpu::TexelCopyTextureInfo {
178 texture,
179 mip_level: 0,
180 origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
181 aspect: wgpu::TextureAspect::All,
182 },
183 pixels,
184 wgpu::TexelCopyBufferLayout {
185 offset: 0,
186 bytes_per_row: Some(bytes_per_pixel(format) * width),
187 rows_per_image: Some(height),
188 },
189 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
190 );
191}
192
193pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
203 use wgpu::TextureFormat as F;
204 match format {
205 F::R8Unorm | F::R8Snorm | F::R8Uint | F::R8Sint => 1,
206 F::R16Uint | F::R16Sint | F::R16Unorm | F::R16Snorm | F::R16Float | F::Rg8Unorm | F::Rg8Snorm
207 | F::Rg8Uint | F::Rg8Sint => 2,
208 F::R32Uint | F::R32Sint | F::R32Float | F::Rg16Uint | F::Rg16Sint | F::Rg16Unorm | F::Rg16Snorm
209 | F::Rg16Float | F::Rgba8Unorm | F::Rgba8UnormSrgb | F::Rgba8Snorm | F::Rgba8Uint | F::Rgba8Sint
210 | F::Bgra8Unorm | F::Bgra8UnormSrgb | F::Rgb10a2Uint | F::Rgb10a2Unorm | F::Rg11b10Ufloat
211 | F::Rgb9e5Ufloat => 4,
212 F::R64Uint | F::Rg32Uint | F::Rg32Sint | F::Rg32Float | F::Rgba16Uint | F::Rgba16Sint
213 | F::Rgba16Unorm | F::Rgba16Snorm | F::Rgba16Float => 8,
214 F::Rgba32Uint | F::Rgba32Sint | F::Rgba32Float => 16,
215 other => panic!(
216 "unsupported texture format for GPUTexture: {other:?} — block-compressed, \
217 multi-planar, and depth/stencil formats have no linear CPU-side pixel layout \
218 this helper can compute"
219 ),
220 }
221}
222
223pub(crate) fn check_texture_dimensions(device: &wgpu::Device, what: &str, width: u32, height: u32) {
230 let max = device.limits().max_texture_dimension_2d;
231 if width > max || height > max {
232 panic!("{what}: {width}x{height} exceeds this device's max_texture_dimension_2d ({max})");
233 }
234}
235
236pub(crate) fn check_texture_array_layers(device: &wgpu::Device, what: &str, layer_count: u32) {
239 let max = device.limits().max_texture_array_layers;
240 if layer_count > max {
241 panic!("{what}: {layer_count} layers exceeds this device's max_texture_array_layers ({max})");
242 }
243}
244
245fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
247 rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
248}
249
250fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
254 rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
255}
256
257fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
260 rgba32f
261 .chunks_exact(4)
262 .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
263 .collect()
264}
265
266fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
268 rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
269}
270
271fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
274 rgba32f
275 .iter()
276 .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
277 .collect()
278}
279
280pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
290 use wgpu::TextureFormat as F;
291
292 let img = match image::open(path) {
293 Ok(img) => img,
294 Err(e) => {
295 tracing::error!("failed to load texture '{path}': {e}");
296 return None;
297 }
298 };
299
300 Some(match format {
301 F::Rgba8Unorm | F::Rgba8UnormSrgb => {
302 let img = img.to_rgba8();
303 let (w, h) = img.dimensions();
304 (w, h, img.into_raw())
305 }
306 F::Bgra8Unorm | F::Bgra8UnormSrgb => {
307 let img = img.to_rgba8();
308 let (w, h) = img.dimensions();
309 (w, h, bgra_swap(&img.into_raw()))
310 }
311 F::R8Unorm => {
312 let img = img.to_rgba8();
313 let (w, h) = img.dimensions();
314 (w, h, take_channels_u8(&img.into_raw(), 1))
315 }
316 F::Rg8Unorm => {
317 let img = img.to_rgba8();
318 let (w, h) = img.dimensions();
319 (w, h, take_channels_u8(&img.into_raw(), 2))
320 }
321 F::Rgba16Unorm => {
322 let img = img.to_rgba32f();
323 let (w, h) = img.dimensions();
324 (w, h, rgba32f_to_unorm16(img.into_raw().as_slice()))
325 }
326 F::Rgba32Float => {
327 let img = img.to_rgba32f();
328 let (w, h) = img.dimensions();
329 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
330 (w, h, bytes)
331 }
332 F::Rg32Float => {
333 let img = img.to_rgba32f();
334 let (w, h) = img.dimensions();
335 (w, h, take_channels_f32(img.into_raw().as_slice(), 2))
336 }
337 F::R32Float => {
338 let img = img.to_rgba32f();
339 let (w, h) = img.dimensions();
340 (w, h, take_channels_f32(img.into_raw().as_slice(), 1))
341 }
342 F::Rgba16Float => {
343 let img = img.to_rgba32f();
344 let (w, h) = img.dimensions();
345 (w, h, take_channels_f16(img.into_raw().as_slice(), 4))
346 }
347 F::Rg16Float => {
348 let img = img.to_rgba32f();
349 let (w, h) = img.dimensions();
350 (w, h, take_channels_f16(img.into_raw().as_slice(), 2))
351 }
352 F::R16Float => {
353 let img = img.to_rgba32f();
354 let (w, h) = img.dimensions();
355 (w, h, take_channels_f16(img.into_raw().as_slice(), 1))
356 }
357 other => panic!(
358 "unsupported texture format for GPUTexture: {other:?} — file decoding covers the \
359 regular 8/16/32-bit unorm and float formats; block-compressed and multi-planar \
360 formats aren't decodable from an ordinary image file this way"
361 ),
362 })
363}
364
365impl Asset<WGPUBackend> for GPUTexture {
366 type Source = Texture;
367 type Deps<'a> = Res<'a, MipmapGenerator>;
368
369 fn upload<'a>(
370 source: &Texture,
371 backend: &WGPUBackend,
372 mipmap_generator: &Res<'a, MipmapGenerator>,
373 ) -> Option<Self> {
374 let (width, height, data) = if let Some(path) = source.file {
376 let (w, h, d) = decode_file(path, source.format.into())?;
377 (w, h, Some(d))
378 } else if let Some(data) = &source.data {
379 (source.width, source.height, Some(data.clone()))
380 } else {
381 (source.width, source.height, None)
383 };
384
385 check_texture_dimensions(&backend.device, "GPUTexture", width, height);
386
387 let mip_count = super::mipmap::mip_count(width.max(height), source.generate_mips);
388
389 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
390 label: None,
391 size: wgpu::Extent3d {
392 width,
393 height,
394 depth_or_array_layers: 1,
395 },
396 mip_level_count: mip_count, sample_count: 1,
398 dimension: wgpu::TextureDimension::D2,
399 format: source.format.into(),
400 usage: super::mipmap::texture_usage(mip_count),
401 view_formats: &[],
402 });
403
404 if let Some(data) = &data {
405 backend.queue.write_texture(
407 wgpu::TexelCopyTextureInfo {
408 texture: &texture,
409 mip_level: 0,
410 origin: wgpu::Origin3d::default(),
411 aspect: wgpu::TextureAspect::All,
412 },
413 data,
414 wgpu::TexelCopyBufferLayout {
415 offset: 0,
416 bytes_per_row: Some(bytes_per_pixel(source.format.into()) * width),
417 rows_per_image: Some(height),
418 },
419 wgpu::Extent3d {
420 width,
421 height,
422 depth_or_array_layers: 1,
423 },
424 );
425 }
426
427 if mip_count > 1 {
428 mipmap_generator.generate_mips(
429 &backend.device,
430 &backend.queue,
431 &texture,
432 source.format.into(),
433 mip_count,
434 1,
435 );
436 }
437
438 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
439 Some(Self {
440 texture,
441 view,
442 width,
443 height,
444 format: source.format,
445 ctx: GpuContext::from_backend(backend),
446 })
447 }
448}
449
450crate::wgpu::plugin_macros::mipmap_asset_plugin! {
451 TexturePlugin, GPUTexture
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::wgpu::test_util::with_device;
463
464 #[test]
465 fn dimensions_within_the_limit_do_not_panic() {
466 with_device!(device, _queue, {
467 check_texture_dimensions(&device, "GPUTexture", 64, 64);
468 });
469 }
470
471 #[test]
472 fn dimensions_exceeding_the_limit_panic() {
473 with_device!(device, _queue, {
474 let too_big = device.limits().max_texture_dimension_2d + 1;
475 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
476 check_texture_dimensions(&device, "GPUTexture", too_big, 64);
477 }));
478 assert!(result.is_err(), "expected a panic for a width exceeding max_texture_dimension_2d");
479 });
480 }
481
482 #[test]
483 fn layer_count_within_the_limit_does_not_panic() {
484 with_device!(device, _queue, {
485 check_texture_array_layers(&device, "GPUTextureArray", 4);
486 });
487 }
488
489 #[test]
490 fn layer_count_exceeding_the_limit_panics() {
491 with_device!(device, _queue, {
492 let too_many = device.limits().max_texture_array_layers + 1;
493 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
494 check_texture_array_layers(&device, "GPUTextureArray", too_many);
495 }));
496 assert!(result.is_err(), "expected a panic for layer_count exceeding max_texture_array_layers");
497 });
498 }
499}