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 {
12 file: Option<&'static str>,
15 width: u32,
17 height: u32,
19 format: TextureFormat,
21 data: Option<Vec<u8>>,
23 generate_mips: bool,
25}
26
27impl Texture {
28 pub fn from_file(path: &'static str) -> Self {
31 Self {
32 file: Some(path),
33 width: 0,
34 height: 0,
35 format: TextureFormat::Rgba8UnormSrgb,
36 data: None,
37 generate_mips: false,
38 }
39 }
40
41 pub fn from_data(width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
43 Self {
44 file: None,
45 width,
46 height,
47 format,
48 data: Some(data),
49 generate_mips: false,
50 }
51 }
52
53 pub fn empty(width: u32, height: u32, format: TextureFormat) -> Self {
56 Self {
57 file: None,
58 width,
59 height,
60 format,
61 data: None,
62 generate_mips: false,
63 }
64 }
65
66 pub fn with_format(mut self, format: TextureFormat) -> Self {
67 self.format = format;
68 self
69 }
70
71 pub fn with_mips(mut self) -> Self {
72 self.generate_mips = true;
73 self
74 }
75
76 fn validate(&self) {
80 if self.data.is_some() && (self.width == 0 || self.height == 0) {
81 tracing::warn!(
82 "Texture::from_data(): width/height is 0 ({}x{}) — did you swap the argument \
83 order, or forget to pass the real dimensions?",
84 self.width,
85 self.height,
86 );
87 }
88 }
89
90 pub fn build(self) -> Self {
92 self.validate();
93 self
94 }
95
96 pub fn build_asset(self, name: &str, assets: &mut Assets<Self>) -> Handle<Self> {
99 self.validate();
100 assets.insert(name, self)
101 }
102}
103
104pub struct GPUTexture {
111 texture: wgpu::Texture,
112 view: wgpu::TextureView,
113 width: u32,
114 height: u32,
115 format: TextureFormat,
116 ctx: GpuContext,
117}
118
119impl GPUTexture {
120 pub fn write(&self, pixels: &[u8]) {
125 write_texture_level0(self.ctx.queue(), &self.texture, 0, self.format.into(), self.width, self.height, pixels);
126 }
127
128 pub fn width(&self) -> u32 {
129 self.width
130 }
131
132 pub fn height(&self) -> u32 {
133 self.height
134 }
135
136 pub(crate) fn view(&self) -> &wgpu::TextureView {
137 &self.view
138 }
139}
140
141pub(crate) fn write_texture_level0(
148 queue: &wgpu::Queue,
149 texture: &wgpu::Texture,
150 origin_z: u32,
151 format: wgpu::TextureFormat,
152 width: u32,
153 height: u32,
154 pixels: &[u8],
155) {
156 queue.write_texture(
157 wgpu::TexelCopyTextureInfo {
158 texture,
159 mip_level: 0,
160 origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
161 aspect: wgpu::TextureAspect::All,
162 },
163 pixels,
164 wgpu::TexelCopyBufferLayout {
165 offset: 0,
166 bytes_per_row: Some(bytes_per_pixel(format) * width),
167 rows_per_image: Some(height),
168 },
169 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
170 );
171}
172
173pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
183 use wgpu::TextureFormat as F;
184 match format {
185 F::R8Unorm | F::R8Snorm | F::R8Uint | F::R8Sint => 1,
186 F::R16Uint | F::R16Sint | F::R16Unorm | F::R16Snorm | F::R16Float | F::Rg8Unorm | F::Rg8Snorm
187 | F::Rg8Uint | F::Rg8Sint => 2,
188 F::R32Uint | F::R32Sint | F::R32Float | F::Rg16Uint | F::Rg16Sint | F::Rg16Unorm | F::Rg16Snorm
189 | F::Rg16Float | F::Rgba8Unorm | F::Rgba8UnormSrgb | F::Rgba8Snorm | F::Rgba8Uint | F::Rgba8Sint
190 | F::Bgra8Unorm | F::Bgra8UnormSrgb | F::Rgb10a2Uint | F::Rgb10a2Unorm | F::Rg11b10Ufloat
191 | F::Rgb9e5Ufloat => 4,
192 F::R64Uint | F::Rg32Uint | F::Rg32Sint | F::Rg32Float | F::Rgba16Uint | F::Rgba16Sint
193 | F::Rgba16Unorm | F::Rgba16Snorm | F::Rgba16Float => 8,
194 F::Rgba32Uint | F::Rgba32Sint | F::Rgba32Float => 16,
195 other => panic!(
196 "unsupported texture format for GPUTexture: {other:?} — block-compressed, \
197 multi-planar, and depth/stencil formats have no linear CPU-side pixel layout \
198 this helper can compute"
199 ),
200 }
201}
202
203pub(crate) fn check_texture_dimensions(device: &wgpu::Device, what: &str, width: u32, height: u32) {
210 let max = device.limits().max_texture_dimension_2d;
211 if width > max || height > max {
212 panic!("{what}: {width}x{height} exceeds this device's max_texture_dimension_2d ({max})");
213 }
214}
215
216pub(crate) fn check_texture_array_layers(device: &wgpu::Device, what: &str, layer_count: u32) {
219 let max = device.limits().max_texture_array_layers;
220 if layer_count > max {
221 panic!("{what}: {layer_count} layers exceeds this device's max_texture_array_layers ({max})");
222 }
223}
224
225fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
227 rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
228}
229
230fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
234 rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
235}
236
237fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
240 rgba32f
241 .chunks_exact(4)
242 .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
243 .collect()
244}
245
246fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
248 rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
249}
250
251fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
254 rgba32f
255 .iter()
256 .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
257 .collect()
258}
259
260pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
270 use wgpu::TextureFormat as F;
271
272 let img = match image::open(path) {
273 Ok(img) => img,
274 Err(e) => {
275 tracing::error!("failed to load texture '{path}': {e}");
276 return None;
277 }
278 };
279
280 Some(match format {
281 F::Rgba8Unorm | F::Rgba8UnormSrgb => {
282 let img = img.to_rgba8();
283 let (w, h) = img.dimensions();
284 (w, h, img.into_raw())
285 }
286 F::Bgra8Unorm | F::Bgra8UnormSrgb => {
287 let img = img.to_rgba8();
288 let (w, h) = img.dimensions();
289 (w, h, bgra_swap(&img.into_raw()))
290 }
291 F::R8Unorm => {
292 let img = img.to_rgba8();
293 let (w, h) = img.dimensions();
294 (w, h, take_channels_u8(&img.into_raw(), 1))
295 }
296 F::Rg8Unorm => {
297 let img = img.to_rgba8();
298 let (w, h) = img.dimensions();
299 (w, h, take_channels_u8(&img.into_raw(), 2))
300 }
301 F::Rgba16Unorm => {
302 let img = img.to_rgba32f();
303 let (w, h) = img.dimensions();
304 (w, h, rgba32f_to_unorm16(img.into_raw().as_slice()))
305 }
306 F::Rgba32Float => {
307 let img = img.to_rgba32f();
308 let (w, h) = img.dimensions();
309 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
310 (w, h, bytes)
311 }
312 F::Rg32Float => {
313 let img = img.to_rgba32f();
314 let (w, h) = img.dimensions();
315 (w, h, take_channels_f32(img.into_raw().as_slice(), 2))
316 }
317 F::R32Float => {
318 let img = img.to_rgba32f();
319 let (w, h) = img.dimensions();
320 (w, h, take_channels_f32(img.into_raw().as_slice(), 1))
321 }
322 F::Rgba16Float => {
323 let img = img.to_rgba32f();
324 let (w, h) = img.dimensions();
325 (w, h, take_channels_f16(img.into_raw().as_slice(), 4))
326 }
327 F::Rg16Float => {
328 let img = img.to_rgba32f();
329 let (w, h) = img.dimensions();
330 (w, h, take_channels_f16(img.into_raw().as_slice(), 2))
331 }
332 F::R16Float => {
333 let img = img.to_rgba32f();
334 let (w, h) = img.dimensions();
335 (w, h, take_channels_f16(img.into_raw().as_slice(), 1))
336 }
337 other => panic!(
338 "unsupported texture format for GPUTexture: {other:?} — file decoding covers the \
339 regular 8/16/32-bit unorm and float formats; block-compressed and multi-planar \
340 formats aren't decodable from an ordinary image file this way"
341 ),
342 })
343}
344
345impl Asset<WGPUBackend> for GPUTexture {
346 type Source = Texture;
347 type Deps<'a> = Res<'a, MipmapGenerator>;
348
349 fn upload<'a>(
350 source: &Texture,
351 backend: &WGPUBackend,
352 mipmap_generator: &Res<'a, MipmapGenerator>,
353 ) -> Option<Self> {
354 let (width, height, data) = if let Some(path) = source.file {
356 let (w, h, d) = decode_file(path, source.format.into())?;
357 (w, h, Some(d))
358 } else if let Some(data) = &source.data {
359 (source.width, source.height, Some(data.clone()))
360 } else {
361 (source.width, source.height, None)
363 };
364
365 check_texture_dimensions(&backend.device, "GPUTexture", width, height);
366
367 let mip_count = super::mipmap::mip_count(width.max(height), source.generate_mips);
368
369 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
370 label: None,
371 size: wgpu::Extent3d {
372 width,
373 height,
374 depth_or_array_layers: 1,
375 },
376 mip_level_count: mip_count, sample_count: 1,
378 dimension: wgpu::TextureDimension::D2,
379 format: source.format.into(),
380 usage: super::mipmap::texture_usage(mip_count),
381 view_formats: &[],
382 });
383
384 if let Some(data) = &data {
385 backend.queue.write_texture(
387 wgpu::TexelCopyTextureInfo {
388 texture: &texture,
389 mip_level: 0,
390 origin: wgpu::Origin3d::default(),
391 aspect: wgpu::TextureAspect::All,
392 },
393 data,
394 wgpu::TexelCopyBufferLayout {
395 offset: 0,
396 bytes_per_row: Some(bytes_per_pixel(source.format.into()) * width),
397 rows_per_image: Some(height),
398 },
399 wgpu::Extent3d {
400 width,
401 height,
402 depth_or_array_layers: 1,
403 },
404 );
405 }
406
407 if mip_count > 1 {
408 mipmap_generator.generate_mips(
409 &backend.device,
410 &backend.queue,
411 &texture,
412 source.format.into(),
413 mip_count,
414 1,
415 );
416 }
417
418 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
419 Some(Self {
420 texture,
421 view,
422 width,
423 height,
424 format: source.format,
425 ctx: GpuContext::from_backend(backend),
426 })
427 }
428}
429
430crate::wgpu::plugin_macros::mipmap_asset_plugin! {
431 TexturePlugin, GPUTexture
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::wgpu::test_util::with_device;
443
444 #[test]
445 fn dimensions_within_the_limit_do_not_panic() {
446 with_device!(device, _queue, {
447 check_texture_dimensions(&device, "GPUTexture", 64, 64);
448 });
449 }
450
451 #[test]
452 fn dimensions_exceeding_the_limit_panic() {
453 with_device!(device, _queue, {
454 let too_big = device.limits().max_texture_dimension_2d + 1;
455 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
456 check_texture_dimensions(&device, "GPUTexture", too_big, 64);
457 }));
458 assert!(result.is_err(), "expected a panic for a width exceeding max_texture_dimension_2d");
459 });
460 }
461
462 #[test]
463 fn layer_count_within_the_limit_does_not_panic() {
464 with_device!(device, _queue, {
465 check_texture_array_layers(&device, "GPUTextureArray", 4);
466 });
467 }
468
469 #[test]
470 fn layer_count_exceeding_the_limit_panics() {
471 with_device!(device, _queue, {
472 let too_many = device.limits().max_texture_array_layers + 1;
473 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
474 check_texture_array_layers(&device, "GPUTextureArray", too_many);
475 }));
476 assert!(result.is_err(), "expected a panic for layer_count exceeding max_texture_array_layers");
477 });
478 }
479}