1use crate::{
2 assets::upload::Asset,
3 ecs::system::Res,
4 wgpu::{backend::WGPUBackend, gpu_context::GpuContext, mipmap::MipmapGenerator, texture_format::TextureFormat},
5};
6
7pub struct TextureDescriptor {
11 pub file: Option<&'static str>,
14 pub width: u32,
16 pub height: u32,
18 pub format: TextureFormat,
20 pub data: Option<Vec<u8>>,
22 pub generate_mips: bool,
24}
25
26impl TextureDescriptor {
27 pub fn from_file(path: &'static str) -> Self {
30 Self {
31 file: Some(path),
32 width: 0,
33 height: 0,
34 format: TextureFormat::Rgba8UnormSrgb,
35 data: None,
36 generate_mips: false,
37 }
38 }
39
40 pub fn from_data(width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
42 Self {
43 file: None,
44 width,
45 height,
46 format,
47 data: Some(data),
48 generate_mips: false,
49 }
50 }
51
52 pub fn with_format(mut self, format: TextureFormat) -> Self {
53 self.format = format;
54 self
55 }
56
57 pub fn with_mips(mut self) -> Self {
58 self.generate_mips = true;
59 self
60 }
61}
62
63pub struct GPUTexture {
70 texture: wgpu::Texture,
71 view: wgpu::TextureView,
72 width: u32,
73 height: u32,
74 format: TextureFormat,
75 ctx: GpuContext,
76}
77
78impl GPUTexture {
79 pub fn write(&self, pixels: &[u8]) {
84 write_texture_level0(self.ctx.queue(), &self.texture, 0, self.format.into(), self.width, self.height, pixels);
85 }
86
87 pub fn width(&self) -> u32 {
88 self.width
89 }
90
91 pub fn height(&self) -> u32 {
92 self.height
93 }
94
95 pub(crate) fn view(&self) -> &wgpu::TextureView {
96 &self.view
97 }
98}
99
100pub(crate) fn write_texture_level0(
107 queue: &wgpu::Queue,
108 texture: &wgpu::Texture,
109 origin_z: u32,
110 format: wgpu::TextureFormat,
111 width: u32,
112 height: u32,
113 pixels: &[u8],
114) {
115 queue.write_texture(
116 wgpu::TexelCopyTextureInfo {
117 texture,
118 mip_level: 0,
119 origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
120 aspect: wgpu::TextureAspect::All,
121 },
122 pixels,
123 wgpu::TexelCopyBufferLayout {
124 offset: 0,
125 bytes_per_row: Some(bytes_per_pixel(format) * width),
126 rows_per_image: Some(height),
127 },
128 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
129 );
130}
131
132pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
142 use wgpu::TextureFormat as F;
143 match format {
144 F::R8Unorm | F::R8Snorm | F::R8Uint | F::R8Sint => 1,
145 F::R16Uint | F::R16Sint | F::R16Unorm | F::R16Snorm | F::R16Float | F::Rg8Unorm | F::Rg8Snorm
146 | F::Rg8Uint | F::Rg8Sint => 2,
147 F::R32Uint | F::R32Sint | F::R32Float | F::Rg16Uint | F::Rg16Sint | F::Rg16Unorm | F::Rg16Snorm
148 | F::Rg16Float | F::Rgba8Unorm | F::Rgba8UnormSrgb | F::Rgba8Snorm | F::Rgba8Uint | F::Rgba8Sint
149 | F::Bgra8Unorm | F::Bgra8UnormSrgb | F::Rgb10a2Uint | F::Rgb10a2Unorm | F::Rg11b10Ufloat
150 | F::Rgb9e5Ufloat => 4,
151 F::R64Uint | F::Rg32Uint | F::Rg32Sint | F::Rg32Float | F::Rgba16Uint | F::Rgba16Sint
152 | F::Rgba16Unorm | F::Rgba16Snorm | F::Rgba16Float => 8,
153 F::Rgba32Uint | F::Rgba32Sint | F::Rgba32Float => 16,
154 other => panic!(
155 "unsupported texture format for GPUTexture: {other:?} — block-compressed, \
156 multi-planar, and depth/stencil formats have no linear CPU-side pixel layout \
157 this helper can compute"
158 ),
159 }
160}
161
162fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
164 rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
165}
166
167fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
171 rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
172}
173
174fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
177 rgba32f
178 .chunks_exact(4)
179 .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
180 .collect()
181}
182
183fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
185 rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
186}
187
188fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
191 rgba32f
192 .iter()
193 .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
194 .collect()
195}
196
197pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
207 use wgpu::TextureFormat as F;
208
209 let img = match image::open(path) {
210 Ok(img) => img,
211 Err(e) => {
212 tracing::error!("failed to load texture '{path}': {e}");
213 return None;
214 }
215 };
216
217 Some(match format {
218 F::Rgba8Unorm | F::Rgba8UnormSrgb => {
219 let img = img.to_rgba8();
220 let (w, h) = img.dimensions();
221 (w, h, img.into_raw())
222 }
223 F::Bgra8Unorm | F::Bgra8UnormSrgb => {
224 let img = img.to_rgba8();
225 let (w, h) = img.dimensions();
226 (w, h, bgra_swap(&img.into_raw()))
227 }
228 F::R8Unorm => {
229 let img = img.to_rgba8();
230 let (w, h) = img.dimensions();
231 (w, h, take_channels_u8(&img.into_raw(), 1))
232 }
233 F::Rg8Unorm => {
234 let img = img.to_rgba8();
235 let (w, h) = img.dimensions();
236 (w, h, take_channels_u8(&img.into_raw(), 2))
237 }
238 F::Rgba16Unorm => {
239 let img = img.to_rgba32f();
240 let (w, h) = img.dimensions();
241 (w, h, rgba32f_to_unorm16(img.into_raw().as_slice()))
242 }
243 F::Rgba32Float => {
244 let img = img.to_rgba32f();
245 let (w, h) = img.dimensions();
246 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
247 (w, h, bytes)
248 }
249 F::Rg32Float => {
250 let img = img.to_rgba32f();
251 let (w, h) = img.dimensions();
252 (w, h, take_channels_f32(img.into_raw().as_slice(), 2))
253 }
254 F::R32Float => {
255 let img = img.to_rgba32f();
256 let (w, h) = img.dimensions();
257 (w, h, take_channels_f32(img.into_raw().as_slice(), 1))
258 }
259 F::Rgba16Float => {
260 let img = img.to_rgba32f();
261 let (w, h) = img.dimensions();
262 (w, h, take_channels_f16(img.into_raw().as_slice(), 4))
263 }
264 F::Rg16Float => {
265 let img = img.to_rgba32f();
266 let (w, h) = img.dimensions();
267 (w, h, take_channels_f16(img.into_raw().as_slice(), 2))
268 }
269 F::R16Float => {
270 let img = img.to_rgba32f();
271 let (w, h) = img.dimensions();
272 (w, h, take_channels_f16(img.into_raw().as_slice(), 1))
273 }
274 other => panic!(
275 "unsupported texture format for GPUTexture: {other:?} — file decoding covers the \
276 regular 8/16/32-bit unorm and float formats; block-compressed and multi-planar \
277 formats aren't decodable from an ordinary image file this way"
278 ),
279 })
280}
281
282impl Asset<WGPUBackend> for GPUTexture {
283 type Source = TextureDescriptor;
284 type Deps<'a> = Res<'a, MipmapGenerator>;
285
286 fn upload<'a>(
287 source: &TextureDescriptor,
288 backend: &WGPUBackend,
289 mipmap_generator: &Res<'a, MipmapGenerator>,
290 ) -> Option<Self> {
291 let (width, height, data) = if let Some(path) = source.file {
293 decode_file(path, source.format.into())?
294 } else if let Some(data) = &source.data {
295 (source.width, source.height, data.clone())
296 } else {
297 tracing::error!("TextureSpec has neither `file` nor `data` set");
298 return None;
299 };
300
301 let mip_count = super::mipmap::mip_count(width.max(height), source.generate_mips);
302
303 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
304 label: None,
305 size: wgpu::Extent3d {
306 width,
307 height,
308 depth_or_array_layers: 1,
309 },
310 mip_level_count: mip_count, sample_count: 1,
312 dimension: wgpu::TextureDimension::D2,
313 format: source.format.into(),
314 usage: super::mipmap::texture_usage(mip_count),
315 view_formats: &[],
316 });
317
318 backend.queue.write_texture(
320 wgpu::TexelCopyTextureInfo {
321 texture: &texture,
322 mip_level: 0,
323 origin: wgpu::Origin3d::default(),
324 aspect: wgpu::TextureAspect::All,
325 },
326 &data,
327 wgpu::TexelCopyBufferLayout {
328 offset: 0,
329 bytes_per_row: Some(bytes_per_pixel(source.format.into()) * width),
330 rows_per_image: Some(height),
331 },
332 wgpu::Extent3d {
333 width,
334 height,
335 depth_or_array_layers: 1,
336 },
337 );
338
339 if mip_count > 1 {
340 mipmap_generator.generate_mips(
341 &backend.device,
342 &backend.queue,
343 &texture,
344 source.format.into(),
345 mip_count,
346 1,
347 );
348 }
349
350 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
351 Some(Self {
352 texture,
353 view,
354 width,
355 height,
356 format: source.format,
357 ctx: GpuContext::from_backend(backend),
358 })
359 }
360}
361
362crate::wgpu::plugin_macros::mipmap_asset_plugin! {
363 TexturePlugin, GPUTexture
369}