1use crate::{
2 assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3 ecs::resources::Read,
4 graphics::{
5 pipeline::{mipmap::{MipLevels, MipmapGenerator}, texture_view::TextureView},
6 render::{Backend, gpu_context::GpuContext},
7 types::{TextureFormat, flags::TextureUsages},
8 },
9};
10
11pub struct Texture {
19 file: Option<&'static str>,
20 width: u32,
21 height: u32,
22 format: TextureFormat,
23 data: Option<Vec<u8>>,
24 mip_levels: MipLevels,
25 sample_count: u32,
26 extra_usage: TextureUsages,
27}
28
29impl Texture {
30 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 mip_levels: MipLevels::None,
38 sample_count: 1,
39 extra_usage: TextureUsages::empty(),
40 }
41 }
42
43 pub fn from_data(width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
44 Self {
45 file: None,
46 width,
47 height,
48 format,
49 data: Some(data),
50 mip_levels: MipLevels::None,
51 sample_count: 1,
52 extra_usage: TextureUsages::empty(),
53 }
54 }
55
56 pub fn empty(width: u32, height: u32, format: TextureFormat) -> Self {
58 Self {
59 file: None,
60 width,
61 height,
62 format,
63 data: None,
64 mip_levels: MipLevels::None,
65 sample_count: 1,
66 extra_usage: TextureUsages::empty(),
67 }
68 }
69
70 pub fn with_sample_count(mut self, count: u32) -> Self {
74 self.sample_count = count;
75 self
76 }
77
78 pub fn with_extra_usage(mut self, usage: TextureUsages) -> Self {
83 self.extra_usage = usage;
84 self
85 }
86
87 pub fn with_format(mut self, format: TextureFormat) -> Self {
88 self.format = format;
89 self
90 }
91
92 pub fn with_mips(mut self) -> Self {
94 self.mip_levels = MipLevels::Full;
95 self
96 }
97
98 pub fn with_mip_count(mut self, count: u32) -> Self {
101 self.mip_levels = MipLevels::Fixed(count);
102 self
103 }
104
105 fn validate(&self) {
106 if self.data.is_some() && (self.width == 0 || self.height == 0) {
107 tracing::warn!(
108 "Texture::from_data(): width/height is 0 ({}x{}) — did you swap the \
109 argument order, or forget to pass the real dimensions?",
110 self.width,
111 self.height,
112 );
113 }
114 }
115
116 pub fn build_asset(self, name: &str, assets: &mut Assets<Texture>) -> Handle<Texture> {
117 self.validate();
118 assets.insert(name, self)
119 }
120
121 pub fn data(&self) -> Option<&[u8]> {
125 self.data.as_deref()
126 }
127
128 pub fn release_cpu_data(&mut self) {
133 self.data = None;
134 }
135}
136
137pub struct GPUTexture {
139 texture: wgpu::Texture,
140 view: wgpu::TextureView,
141 width: u32,
142 height: u32,
143 format: TextureFormat,
144 ctx: GpuContext,
145}
146
147impl GPUTexture {
148 pub fn write(&self, mip_level: u32, pixels: &[u8]) {
151 write_texture_mip(self.ctx.queue(), &self.texture, 0, mip_level, self.format.into(), self.width, self.height, pixels);
152 }
153
154 pub fn width(&self) -> u32 {
155 self.width
156 }
157
158 pub fn height(&self) -> u32 {
159 self.height
160 }
161
162 pub fn get_view(&self, mip_level: u32) -> TextureView {
165 let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
166 dimension: Some(wgpu::TextureViewDimension::D2),
167 base_mip_level: mip_level,
168 mip_level_count: Some(1),
169 ..Default::default()
170 });
171 TextureView::from_raw(view, self.texture.clone())
172 }
173
174 pub(crate) fn view(&self) -> &wgpu::TextureView {
175 &self.view
176 }
177}
178
179pub(crate) fn write_texture_mip(
180 queue: &wgpu::Queue,
181 texture: &wgpu::Texture,
182 origin_z: u32,
183 mip_level: u32,
184 format: wgpu::TextureFormat,
185 width: u32,
186 height: u32,
187 pixels: &[u8],
188) {
189 let width = (width >> mip_level).max(1);
190 let height = (height >> mip_level).max(1);
191 queue.write_texture(
192 wgpu::TexelCopyTextureInfo {
193 texture,
194 mip_level,
195 origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
196 aspect: wgpu::TextureAspect::All,
197 },
198 pixels,
199 wgpu::TexelCopyBufferLayout {
200 offset: 0,
201 bytes_per_row: Some(bytes_per_pixel(format) * width),
202 rows_per_image: Some(height),
203 },
204 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
205 );
206}
207
208pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
209 use wgpu::TextureFormat as F;
210 match format {
211 F::R8Unorm | F::R8Snorm | F::R8Uint | F::R8Sint => 1,
212 F::R16Uint | F::R16Sint | F::R16Unorm | F::R16Snorm | F::R16Float | F::Rg8Unorm | F::Rg8Snorm
213 | F::Rg8Uint | F::Rg8Sint => 2,
214 F::R32Uint | F::R32Sint | F::R32Float | F::Rg16Uint | F::Rg16Sint | F::Rg16Unorm | F::Rg16Snorm
215 | F::Rg16Float | F::Rgba8Unorm | F::Rgba8UnormSrgb | F::Rgba8Snorm | F::Rgba8Uint | F::Rgba8Sint
216 | F::Bgra8Unorm | F::Bgra8UnormSrgb | F::Rgb10a2Uint | F::Rgb10a2Unorm | F::Rg11b10Ufloat
217 | F::Rgb9e5Ufloat => 4,
218 F::R64Uint | F::Rg32Uint | F::Rg32Sint | F::Rg32Float | F::Rgba16Uint | F::Rgba16Sint
219 | F::Rgba16Unorm | F::Rgba16Snorm | F::Rgba16Float => 8,
220 F::Rgba32Uint | F::Rgba32Sint | F::Rgba32Float => 16,
221 other => panic!(
222 "unsupported texture format for GPUTexture: {other:?} — block-compressed, \
223 multi-planar, and depth/stencil formats have no linear CPU-side pixel layout \
224 this helper can compute"
225 ),
226 }
227}
228
229pub(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) {
237 let max = device.limits().max_texture_array_layers;
238 if layer_count > max {
239 panic!("{what}: {layer_count} layers exceeds this device's max_texture_array_layers ({max})");
240 }
241}
242
243fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
244 rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
245}
246
247fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
248 rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
249}
250
251fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
252 rgba32f
253 .chunks_exact(4)
254 .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
255 .collect()
256}
257
258fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
259 rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
260}
261
262fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
263 rgba32f
264 .iter()
265 .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
266 .collect()
267}
268
269pub(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 AssetSource for Texture {
346 type Processed = GPUTexture;
347}
348
349impl Asset<Backend> for Texture {
350 type Deps<'a> = Read<'a, MipmapGenerator>;
351
352 fn upload<'a>(&self, backend: &Backend, mipmap_generator: &Read<'a, MipmapGenerator>) -> Option<GPUTexture> {
353 let (width, height, data) = if let Some(path) = self.file {
354 let (w, h, d) = decode_file(path, self.format.into())?;
355 (w, h, Some(d))
356 } else if let Some(data) = &self.data {
357 (self.width, self.height, Some(data.clone()))
358 } else {
359 (self.width, self.height, None)
360 };
361
362 check_texture_dimensions(&backend.device, "GPUTexture", width, height);
363
364 let mip_count = crate::graphics::pipeline::mipmap::mip_count(width.max(height), self.mip_levels);
365 let usage = crate::graphics::pipeline::mipmap::texture_usage_for(mip_count, data.is_some()) | self.extra_usage.into();
366
367 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
368 label: None,
369 size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
370 mip_level_count: mip_count,
371 sample_count: self.sample_count,
372 dimension: wgpu::TextureDimension::D2,
373 format: self.format.into(),
374 usage,
375 view_formats: &[],
376 });
377
378 if let Some(data) = &data {
379 write_texture_mip(&backend.queue, &texture, 0, 0, self.format.into(), width, height, data);
380
381 if mip_count > 1 {
382 mipmap_generator.generate_mips(backend, &texture, self.format.into(), mip_count, 1);
383 }
384 }
385
386 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
387 Some(GPUTexture { texture, view, width, height, format: self.format, ctx: GpuContext::from_backend(backend) })
388 }
389}