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,
8 },
9};
10
11pub struct Texture {
15 file: Option<&'static str>,
16 width: u32,
17 height: u32,
18 format: TextureFormat,
19 data: Option<Vec<u8>>,
20 mip_levels: MipLevels,
21}
22
23impl Texture {
24 pub fn from_file(path: &'static str) -> Self {
25 Self { file: Some(path), width: 0, height: 0, format: TextureFormat::Rgba8UnormSrgb, data: None, mip_levels: MipLevels::None }
26 }
27
28 pub fn from_data(width: u32, height: u32, format: TextureFormat, data: Vec<u8>) -> Self {
29 Self { file: None, width, height, format, data: Some(data), mip_levels: MipLevels::None }
30 }
31
32 pub fn empty(width: u32, height: u32, format: TextureFormat) -> Self {
34 Self { file: None, width, height, format, data: None, mip_levels: MipLevels::None }
35 }
36
37 pub fn with_format(mut self, format: TextureFormat) -> Self {
38 self.format = format;
39 self
40 }
41
42 pub fn with_mips(mut self) -> Self {
44 self.mip_levels = MipLevels::Full;
45 self
46 }
47
48 pub fn with_mip_count(mut self, count: u32) -> Self {
51 self.mip_levels = MipLevels::Fixed(count);
52 self
53 }
54
55 fn validate(&self) {
56 if self.data.is_some() && (self.width == 0 || self.height == 0) {
57 tracing::warn!(
58 "Texture::from_data(): width/height is 0 ({}x{}) — did you swap the \
59 argument order, or forget to pass the real dimensions?",
60 self.width,
61 self.height,
62 );
63 }
64 }
65
66 pub fn build_asset(self, name: &str, assets: &mut Assets<Texture>) -> Handle<Texture> {
67 self.validate();
68 assets.insert(name, self)
69 }
70
71 pub fn data(&self) -> Option<&[u8]> {
75 self.data.as_deref()
76 }
77
78 pub fn release_cpu_data(&mut self) {
83 self.data = None;
84 }
85}
86
87pub struct GPUTexture {
89 texture: wgpu::Texture,
90 view: wgpu::TextureView,
91 width: u32,
92 height: u32,
93 format: TextureFormat,
94 ctx: GpuContext,
95}
96
97impl GPUTexture {
98 pub fn write(&self, mip_level: u32, pixels: &[u8]) {
101 write_texture_mip(self.ctx.queue(), &self.texture, 0, mip_level, self.format.into(), self.width, self.height, pixels);
102 }
103
104 pub fn width(&self) -> u32 {
105 self.width
106 }
107
108 pub fn height(&self) -> u32 {
109 self.height
110 }
111
112 pub fn get_view(&self, mip_level: u32) -> TextureView {
115 let view = self.texture.create_view(&wgpu::TextureViewDescriptor {
116 dimension: Some(wgpu::TextureViewDimension::D2),
117 base_mip_level: mip_level,
118 mip_level_count: Some(1),
119 ..Default::default()
120 });
121 TextureView::from_raw(view, self.texture.clone())
122 }
123
124 pub(crate) fn view(&self) -> &wgpu::TextureView {
125 &self.view
126 }
127}
128
129pub(crate) fn write_texture_mip(
130 queue: &wgpu::Queue,
131 texture: &wgpu::Texture,
132 origin_z: u32,
133 mip_level: u32,
134 format: wgpu::TextureFormat,
135 width: u32,
136 height: u32,
137 pixels: &[u8],
138) {
139 let width = (width >> mip_level).max(1);
140 let height = (height >> mip_level).max(1);
141 queue.write_texture(
142 wgpu::TexelCopyTextureInfo {
143 texture,
144 mip_level,
145 origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
146 aspect: wgpu::TextureAspect::All,
147 },
148 pixels,
149 wgpu::TexelCopyBufferLayout {
150 offset: 0,
151 bytes_per_row: Some(bytes_per_pixel(format) * width),
152 rows_per_image: Some(height),
153 },
154 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
155 );
156}
157
158pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
159 use wgpu::TextureFormat as F;
160 match format {
161 F::R8Unorm | F::R8Snorm | F::R8Uint | F::R8Sint => 1,
162 F::R16Uint | F::R16Sint | F::R16Unorm | F::R16Snorm | F::R16Float | F::Rg8Unorm | F::Rg8Snorm
163 | F::Rg8Uint | F::Rg8Sint => 2,
164 F::R32Uint | F::R32Sint | F::R32Float | F::Rg16Uint | F::Rg16Sint | F::Rg16Unorm | F::Rg16Snorm
165 | F::Rg16Float | F::Rgba8Unorm | F::Rgba8UnormSrgb | F::Rgba8Snorm | F::Rgba8Uint | F::Rgba8Sint
166 | F::Bgra8Unorm | F::Bgra8UnormSrgb | F::Rgb10a2Uint | F::Rgb10a2Unorm | F::Rg11b10Ufloat
167 | F::Rgb9e5Ufloat => 4,
168 F::R64Uint | F::Rg32Uint | F::Rg32Sint | F::Rg32Float | F::Rgba16Uint | F::Rgba16Sint
169 | F::Rgba16Unorm | F::Rgba16Snorm | F::Rgba16Float => 8,
170 F::Rgba32Uint | F::Rgba32Sint | F::Rgba32Float => 16,
171 other => panic!(
172 "unsupported texture format for GPUTexture: {other:?} — block-compressed, \
173 multi-planar, and depth/stencil formats have no linear CPU-side pixel layout \
174 this helper can compute"
175 ),
176 }
177}
178
179pub(crate) fn check_texture_dimensions(device: &wgpu::Device, what: &str, width: u32, height: u32) {
180 let max = device.limits().max_texture_dimension_2d;
181 if width > max || height > max {
182 panic!("{what}: {width}x{height} exceeds this device's max_texture_dimension_2d ({max})");
183 }
184}
185
186pub(crate) fn check_texture_array_layers(device: &wgpu::Device, what: &str, layer_count: u32) {
187 let max = device.limits().max_texture_array_layers;
188 if layer_count > max {
189 panic!("{what}: {layer_count} layers exceeds this device's max_texture_array_layers ({max})");
190 }
191}
192
193fn take_channels_u8(rgba: &[u8], channels: usize) -> Vec<u8> {
194 rgba.chunks_exact(4).flat_map(|p| p[..channels].to_vec()).collect()
195}
196
197fn bgra_swap(rgba: &[u8]) -> Vec<u8> {
198 rgba.chunks_exact(4).flat_map(|p| [p[2], p[1], p[0], p[3]]).collect()
199}
200
201fn take_channels_f16(rgba32f: &[f32], channels: usize) -> Vec<u8> {
202 rgba32f
203 .chunks_exact(4)
204 .flat_map(|p| p[..channels].iter().flat_map(|c| half::f16::from_f32(*c).to_le_bytes()))
205 .collect()
206}
207
208fn take_channels_f32(rgba32f: &[f32], channels: usize) -> Vec<u8> {
209 rgba32f.chunks_exact(4).flat_map(|p| bytemuck::cast_slice(&p[..channels]).to_vec()).collect()
210}
211
212fn rgba32f_to_unorm16(rgba32f: &[f32]) -> Vec<u8> {
213 rgba32f
214 .iter()
215 .flat_map(|c| ((c.clamp(0.0, 1.0) * 65535.0).round() as u16).to_le_bytes())
216 .collect()
217}
218
219pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
220 use wgpu::TextureFormat as F;
221
222 let img = match image::open(path) {
223 Ok(img) => img,
224 Err(e) => {
225 tracing::error!("failed to load texture '{path}': {e}");
226 return None;
227 }
228 };
229
230 Some(match format {
231 F::Rgba8Unorm | F::Rgba8UnormSrgb => {
232 let img = img.to_rgba8();
233 let (w, h) = img.dimensions();
234 (w, h, img.into_raw())
235 }
236 F::Bgra8Unorm | F::Bgra8UnormSrgb => {
237 let img = img.to_rgba8();
238 let (w, h) = img.dimensions();
239 (w, h, bgra_swap(&img.into_raw()))
240 }
241 F::R8Unorm => {
242 let img = img.to_rgba8();
243 let (w, h) = img.dimensions();
244 (w, h, take_channels_u8(&img.into_raw(), 1))
245 }
246 F::Rg8Unorm => {
247 let img = img.to_rgba8();
248 let (w, h) = img.dimensions();
249 (w, h, take_channels_u8(&img.into_raw(), 2))
250 }
251 F::Rgba16Unorm => {
252 let img = img.to_rgba32f();
253 let (w, h) = img.dimensions();
254 (w, h, rgba32f_to_unorm16(img.into_raw().as_slice()))
255 }
256 F::Rgba32Float => {
257 let img = img.to_rgba32f();
258 let (w, h) = img.dimensions();
259 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
260 (w, h, bytes)
261 }
262 F::Rg32Float => {
263 let img = img.to_rgba32f();
264 let (w, h) = img.dimensions();
265 (w, h, take_channels_f32(img.into_raw().as_slice(), 2))
266 }
267 F::R32Float => {
268 let img = img.to_rgba32f();
269 let (w, h) = img.dimensions();
270 (w, h, take_channels_f32(img.into_raw().as_slice(), 1))
271 }
272 F::Rgba16Float => {
273 let img = img.to_rgba32f();
274 let (w, h) = img.dimensions();
275 (w, h, take_channels_f16(img.into_raw().as_slice(), 4))
276 }
277 F::Rg16Float => {
278 let img = img.to_rgba32f();
279 let (w, h) = img.dimensions();
280 (w, h, take_channels_f16(img.into_raw().as_slice(), 2))
281 }
282 F::R16Float => {
283 let img = img.to_rgba32f();
284 let (w, h) = img.dimensions();
285 (w, h, take_channels_f16(img.into_raw().as_slice(), 1))
286 }
287 other => panic!(
288 "unsupported texture format for GPUTexture: {other:?} — file decoding covers the \
289 regular 8/16/32-bit unorm and float formats; block-compressed and multi-planar \
290 formats aren't decodable from an ordinary image file this way"
291 ),
292 })
293}
294
295impl AssetSource for Texture {
296 type Processed = GPUTexture;
297}
298
299impl Asset<Backend> for Texture {
300 type Deps<'a> = Read<'a, MipmapGenerator>;
301
302 fn upload<'a>(&self, backend: &Backend, mipmap_generator: &Read<'a, MipmapGenerator>) -> Option<GPUTexture> {
303 let (width, height, data) = if let Some(path) = self.file {
304 let (w, h, d) = decode_file(path, self.format.into())?;
305 (w, h, Some(d))
306 } else if let Some(data) = &self.data {
307 (self.width, self.height, Some(data.clone()))
308 } else {
309 (self.width, self.height, None)
310 };
311
312 check_texture_dimensions(&backend.device, "GPUTexture", width, height);
313
314 let mip_count = crate::graphics::pipeline::mipmap::mip_count(width.max(height), self.mip_levels);
315 let usage = crate::graphics::pipeline::mipmap::texture_usage_for(mip_count, data.is_some());
316
317 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
318 label: None,
319 size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
320 mip_level_count: mip_count,
321 sample_count: 1,
322 dimension: wgpu::TextureDimension::D2,
323 format: self.format.into(),
324 usage,
325 view_formats: &[],
326 });
327
328 if let Some(data) = &data {
329 write_texture_mip(&backend.queue, &texture, 0, 0, self.format.into(), width, height, data);
330
331 if mip_count > 1 {
332 mipmap_generator.generate_mips(backend, &texture, self.format.into(), mip_count, 1);
333 }
334 }
335
336 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
337 Some(GPUTexture { texture, view, width, height, format: self.format, ctx: GpuContext::from_backend(backend) })
338 }
339}