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
203fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
205 rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
206}
207
208fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
212 rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
213}
214
215fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
218 rgba32f
219 .chunks_exact(4)
220 .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
221 .collect()
222}
223
224fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
226 rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
227}
228
229fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
232 rgba32f
233 .iter()
234 .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
235 .collect()
236}
237
238pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
248 use wgpu::TextureFormat as F;
249
250 let img = match image::open(path) {
251 Ok(img) => img,
252 Err(e) => {
253 tracing::error!("failed to load texture '{path}': {e}");
254 return None;
255 }
256 };
257
258 Some(match format {
259 F::Rgba8Unorm | F::Rgba8UnormSrgb => {
260 let img = img.to_rgba8();
261 let (w, h) = img.dimensions();
262 (w, h, img.into_raw())
263 }
264 F::Bgra8Unorm | F::Bgra8UnormSrgb => {
265 let img = img.to_rgba8();
266 let (w, h) = img.dimensions();
267 (w, h, bgra_swap(&img.into_raw()))
268 }
269 F::R8Unorm => {
270 let img = img.to_rgba8();
271 let (w, h) = img.dimensions();
272 (w, h, take_channels_u8(&img.into_raw(), 1))
273 }
274 F::Rg8Unorm => {
275 let img = img.to_rgba8();
276 let (w, h) = img.dimensions();
277 (w, h, take_channels_u8(&img.into_raw(), 2))
278 }
279 F::Rgba16Unorm => {
280 let img = img.to_rgba32f();
281 let (w, h) = img.dimensions();
282 (w, h, rgba32f_to_unorm16(img.into_raw().as_slice()))
283 }
284 F::Rgba32Float => {
285 let img = img.to_rgba32f();
286 let (w, h) = img.dimensions();
287 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
288 (w, h, bytes)
289 }
290 F::Rg32Float => {
291 let img = img.to_rgba32f();
292 let (w, h) = img.dimensions();
293 (w, h, take_channels_f32(img.into_raw().as_slice(), 2))
294 }
295 F::R32Float => {
296 let img = img.to_rgba32f();
297 let (w, h) = img.dimensions();
298 (w, h, take_channels_f32(img.into_raw().as_slice(), 1))
299 }
300 F::Rgba16Float => {
301 let img = img.to_rgba32f();
302 let (w, h) = img.dimensions();
303 (w, h, take_channels_f16(img.into_raw().as_slice(), 4))
304 }
305 F::Rg16Float => {
306 let img = img.to_rgba32f();
307 let (w, h) = img.dimensions();
308 (w, h, take_channels_f16(img.into_raw().as_slice(), 2))
309 }
310 F::R16Float => {
311 let img = img.to_rgba32f();
312 let (w, h) = img.dimensions();
313 (w, h, take_channels_f16(img.into_raw().as_slice(), 1))
314 }
315 other => panic!(
316 "unsupported texture format for GPUTexture: {other:?} — file decoding covers the \
317 regular 8/16/32-bit unorm and float formats; block-compressed and multi-planar \
318 formats aren't decodable from an ordinary image file this way"
319 ),
320 })
321}
322
323impl Asset<WGPUBackend> for GPUTexture {
324 type Source = Texture;
325 type Deps<'a> = Res<'a, MipmapGenerator>;
326
327 fn upload<'a>(
328 source: &Texture,
329 backend: &WGPUBackend,
330 mipmap_generator: &Res<'a, MipmapGenerator>,
331 ) -> Option<Self> {
332 let (width, height, data) = if let Some(path) = source.file {
334 let (w, h, d) = decode_file(path, source.format.into())?;
335 (w, h, Some(d))
336 } else if let Some(data) = &source.data {
337 (source.width, source.height, Some(data.clone()))
338 } else {
339 (source.width, source.height, None)
341 };
342
343 let mip_count = super::mipmap::mip_count(width.max(height), source.generate_mips);
344
345 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
346 label: None,
347 size: wgpu::Extent3d {
348 width,
349 height,
350 depth_or_array_layers: 1,
351 },
352 mip_level_count: mip_count, sample_count: 1,
354 dimension: wgpu::TextureDimension::D2,
355 format: source.format.into(),
356 usage: super::mipmap::texture_usage(mip_count),
357 view_formats: &[],
358 });
359
360 if let Some(data) = &data {
361 backend.queue.write_texture(
363 wgpu::TexelCopyTextureInfo {
364 texture: &texture,
365 mip_level: 0,
366 origin: wgpu::Origin3d::default(),
367 aspect: wgpu::TextureAspect::All,
368 },
369 data,
370 wgpu::TexelCopyBufferLayout {
371 offset: 0,
372 bytes_per_row: Some(bytes_per_pixel(source.format.into()) * width),
373 rows_per_image: Some(height),
374 },
375 wgpu::Extent3d {
376 width,
377 height,
378 depth_or_array_layers: 1,
379 },
380 );
381 }
382
383 if mip_count > 1 {
384 mipmap_generator.generate_mips(
385 &backend.device,
386 &backend.queue,
387 &texture,
388 source.format.into(),
389 mip_count,
390 1,
391 );
392 }
393
394 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
395 Some(Self {
396 texture,
397 view,
398 width,
399 height,
400 format: source.format,
401 ctx: GpuContext::from_backend(backend),
402 })
403 }
404}
405
406crate::wgpu::plugin_macros::mipmap_asset_plugin! {
407 TexturePlugin, GPUTexture
413}