blade_render/texture/
mod.rs

1use std::{
2    fmt, io, mem, ptr, slice, str,
3    sync::{Arc, Mutex},
4};
5
6#[repr(transparent)]
7#[derive(Clone, Copy, Debug, blade_macros::Flat)]
8struct TextureFormatWrap(blade_graphics::TextureFormat);
9
10#[derive(blade_macros::Flat)]
11struct CookedMip<'a> {
12    data: &'a [u8],
13}
14
15#[derive(blade_macros::Flat)]
16pub struct CookedImage<'a> {
17    name: &'a [u8],
18    extent: [u32; 3],
19    format: TextureFormatWrap,
20    mips: Vec<CookedMip<'a>>,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24pub struct Meta {
25    pub format: blade_graphics::TextureFormat,
26    pub generate_mips: bool,
27    pub y_flip: bool,
28}
29
30impl fmt::Display for Meta {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        fmt::Debug::fmt(&self.format, f)
33    }
34}
35
36pub struct Texture {
37    pub object: blade_graphics::Texture,
38    pub view: blade_graphics::TextureView,
39    pub extent: blade_graphics::Extent,
40}
41
42struct Initialization {
43    dst: blade_graphics::Texture,
44}
45
46struct Transfer {
47    stage: blade_graphics::Buffer,
48    bytes_per_row: u32,
49    dst: blade_graphics::Texture,
50    extent: blade_graphics::Extent,
51    mip_level: u32,
52}
53
54//TODO: consider this to be shared within the `AssetHub`?
55#[derive(Default)]
56struct PendingOperations {
57    initializations: Vec<Initialization>,
58    transfers: Vec<Transfer>,
59}
60
61pub struct Baker {
62    gpu_context: Arc<blade_graphics::Context>,
63    pending_operations: Mutex<PendingOperations>,
64}
65
66impl Baker {
67    pub fn new(gpu_context: &Arc<blade_graphics::Context>) -> Self {
68        Self {
69            gpu_context: Arc::clone(gpu_context),
70            pending_operations: Mutex::new(PendingOperations::default()),
71        }
72    }
73
74    pub fn flush(
75        &self,
76        encoder: &mut blade_graphics::CommandEncoder,
77        temp_buffers: &mut Vec<blade_graphics::Buffer>,
78    ) {
79        let mut pending_ops = self.pending_operations.lock().unwrap();
80        for init in pending_ops.initializations.drain(..) {
81            encoder.init_texture(init.dst);
82        }
83        if !pending_ops.transfers.is_empty() {
84            let mut pass = encoder.transfer("init textures");
85            for transfer in pending_ops.transfers.drain(..) {
86                let dst = blade_graphics::TexturePiece {
87                    texture: transfer.dst,
88                    mip_level: transfer.mip_level,
89                    array_layer: 0,
90                    origin: [0; 3],
91                };
92                pass.copy_buffer_to_texture(
93                    transfer.stage.into(),
94                    transfer.bytes_per_row,
95                    dst,
96                    transfer.extent,
97                );
98                temp_buffers.push(transfer.stage);
99            }
100        }
101    }
102}
103
104impl blade_asset::Baker for Baker {
105    type Meta = Meta;
106    type Data<'a> = CookedImage<'a>;
107    type Output = Texture;
108    fn cook(
109        &self,
110        source: &[u8],
111        extension: &str,
112        meta: Meta,
113        cooker: Arc<blade_asset::Cooker<Self>>,
114        exe_context: &choir::ExecutionContext,
115    ) {
116        use blade_graphics::TextureFormat as Tf;
117
118        type LdrTexel = [u8; 4];
119        type HdrTexel = [f32; 3];
120        enum PlainData {
121            Ldr(Vec<LdrTexel>),
122            Hdr(Vec<HdrTexel>),
123        }
124        struct PlainImage {
125            width: usize,
126            height: usize,
127            data: PlainData,
128        }
129
130        let src: PlainImage = match extension {
131            #[cfg(feature = "asset")]
132            "png" => {
133                profiling::scope!("decode png");
134                let options =
135                    zune_core::options::DecoderOptions::default().png_set_add_alpha_channel(true);
136                let mut decoder = zune_png::PngDecoder::new_with_options(source, options);
137                decoder.decode_headers().unwrap();
138                let info = decoder.get_info().unwrap().clone();
139                let mut data = vec![[0u8; 4]; info.width * info.height];
140                let count = data.len() * data[0].len();
141                assert_eq!(count, decoder.output_buffer_size().unwrap());
142                decoder
143                    .decode_into(unsafe {
144                        slice::from_raw_parts_mut(data.as_mut_ptr() as *mut u8, count)
145                    })
146                    .unwrap();
147                PlainImage {
148                    width: info.width,
149                    height: info.height,
150                    data: PlainData::Ldr(data),
151                }
152            }
153            #[cfg(feature = "asset")]
154            "jpg" | "jpeg" => {
155                profiling::scope!("decode jpeg");
156                let options = zune_core::options::DecoderOptions::default()
157                    .jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::RGBA);
158                let mut decoder = zune_jpeg::JpegDecoder::new_with_options(source, options);
159                decoder.decode_headers().unwrap();
160                let info = decoder.info().unwrap();
161                let mut data = vec![[0u8; 4]; info.width as usize * info.height as usize];
162                let count = data.len() * data[0].len();
163                assert_eq!(count, decoder.output_buffer_size().unwrap());
164                decoder
165                    .decode_into(unsafe {
166                        slice::from_raw_parts_mut(data.as_mut_ptr() as *mut u8, count)
167                    })
168                    .unwrap();
169                PlainImage {
170                    width: info.width as usize,
171                    height: info.height as usize,
172                    data: PlainData::Ldr(data),
173                }
174            }
175            #[cfg(feature = "asset")]
176            "hdr" => {
177                profiling::scope!("decode hdr");
178                let options = zune_core::options::DecoderOptions::default();
179                let mut decoder = zune_hdr::HdrDecoder::new_with_options(source, options);
180                decoder.decode_headers().unwrap();
181                let (width, height) = decoder.get_dimensions().unwrap();
182                let colorspace = decoder.get_colorspace().unwrap();
183                assert_eq!(colorspace, zune_core::colorspace::ColorSpace::RGB);
184                let mut data = vec![[0f32; 3]; width * height];
185                let count = data.len() * data[0].len();
186                assert_eq!(count, decoder.output_buffer_size().unwrap());
187                decoder
188                    .decode_into(unsafe {
189                        slice::from_raw_parts_mut(data.as_mut_ptr() as *mut f32, count)
190                    })
191                    .unwrap();
192                PlainImage {
193                    width,
194                    height,
195                    data: PlainData::Hdr(data),
196                }
197            }
198            #[cfg(feature = "asset")]
199            "exr" => {
200                use exr::prelude::{ReadChannels as _, ReadLayers as _};
201                profiling::scope!("decode exr");
202                struct RawImage {
203                    width: usize,
204                    data: Vec<HdrTexel>,
205                }
206                let image = exr::image::read::read()
207                    .no_deep_data()
208                    .largest_resolution_level()
209                    .rgba_channels(
210                        |size, _| RawImage {
211                            width: size.width(),
212                            data: vec![[0f32; 3]; size.width() * size.height()],
213                        },
214                        |image, position, (r, g, b, _): (f32, f32, f32, f32)| {
215                            image.data[position.y() * image.width + position.x()] = [r, g, b];
216                        },
217                    )
218                    .first_valid_layer()
219                    .all_attributes()
220                    .from_buffered(io::Cursor::new(source))
221                    .unwrap();
222                PlainImage {
223                    width: image.layer_data.size.width(),
224                    height: image.layer_data.size.height(),
225                    data: PlainData::Hdr(image.layer_data.channel_data.pixels.data),
226                }
227            }
228            other => panic!("Unknown texture extension: {}", other),
229        };
230
231        #[cfg(feature = "asset")]
232        match src.data {
233            PlainData::Ldr(mut data) => {
234                if meta.y_flip {
235                    profiling::scope!("y-flip");
236                    zune_imageprocs::flip::vertical_flip(&mut data, src.width);
237                }
238
239                let dst_format = match meta.format {
240                    Tf::Bc1Unorm | Tf::Bc1UnormSrgb => texpresso::Format::Bc1,
241                    Tf::Bc2Unorm | Tf::Bc2UnormSrgb => texpresso::Format::Bc2,
242                    Tf::Bc3Unorm | Tf::Bc3UnormSrgb => texpresso::Format::Bc3,
243                    Tf::Bc4Unorm | Tf::Bc4Snorm => texpresso::Format::Bc4,
244                    Tf::Bc5Unorm | Tf::Bc5Snorm => texpresso::Format::Bc5,
245                    other => panic!("Unsupported destination format {:?}", other),
246                };
247
248                let mut src_mips = vec![data];
249                let mut mips = {
250                    let compressed_size = dst_format.compressed_size(src.width, src.height);
251                    vec![vec![0u8; compressed_size]]
252                };
253                let base_extent = blade_graphics::Extent {
254                    width: src.width as u32,
255                    height: src.height as u32,
256                    depth: 1,
257                };
258                if meta.generate_mips {
259                    profiling::scope!("generate mipmap");
260                    for i in 1..base_extent.max_mip_levels() {
261                        let prev_extent = base_extent.at_mip_level(i - 1);
262                        let cur_extent = base_extent.at_mip_level(i);
263                        let prev_data = src_mips.last().unwrap();
264                        let prev_raw = unsafe {
265                            slice::from_raw_parts(
266                                prev_data.as_ptr() as *const u8,
267                                prev_data.len() * 4,
268                            )
269                        };
270                        let mut cur_data =
271                            vec![[0u8; 4]; cur_extent.width as usize * cur_extent.height as usize];
272                        let cur_raw = unsafe {
273                            slice::from_raw_parts_mut(
274                                cur_data.as_mut_ptr() as *mut u8,
275                                cur_data.len() * 4,
276                            )
277                        };
278                        zune_imageprocs::resize::resize(
279                            prev_raw,
280                            cur_raw,
281                            zune_imageprocs::resize::ResizeMethod::Bilinear,
282                            prev_extent.width as _,
283                            prev_extent.height as _,
284                            cur_extent.width as _,
285                            cur_extent.height as _,
286                        );
287                        src_mips.push(cur_data);
288                        let compressed_size = dst_format
289                            .compressed_size(cur_extent.width as _, cur_extent.height as _);
290                        mips.push(vec![0u8; compressed_size]);
291                    }
292                }
293
294                struct CompressTask {
295                    src: Vec<LdrTexel>,
296                    dst_ptr: *mut u8,
297                }
298                unsafe impl Send for CompressTask {}
299                unsafe impl Sync for CompressTask {}
300
301                let compress_task = exe_context
302                    .fork("compress")
303                    .init_iter(
304                        src_mips
305                            .into_iter()
306                            .zip(mips.iter_mut())
307                            .map(|(src, dst)| CompressTask {
308                                src,
309                                dst_ptr: dst.as_mut_ptr(),
310                            })
311                            .enumerate(),
312                        move |_, (i, task)| {
313                            let extent = base_extent.at_mip_level(i as u32);
314                            let compressed_size =
315                                dst_format.compressed_size(extent.width as _, extent.height as _);
316                            let params = texpresso::Params {
317                                //TODO: make this configurable
318                                algorithm: texpresso::Algorithm::RangeFit,
319                                ..Default::default()
320                            };
321                            let dst =
322                                unsafe { slice::from_raw_parts_mut(task.dst_ptr, compressed_size) };
323                            let raw = unsafe {
324                                slice::from_raw_parts(
325                                    task.src.as_ptr() as *const u8,
326                                    task.src.len() * 4,
327                                )
328                            };
329                            dst_format.compress(
330                                raw,
331                                extent.width as _,
332                                extent.height as _,
333                                params,
334                                dst,
335                            );
336                        },
337                    )
338                    .run();
339
340                exe_context
341                    .fork("finish")
342                    .init(move |_| {
343                        cooker.finish(CookedImage {
344                            name: &[],
345                            extent: [base_extent.width, base_extent.height, base_extent.depth],
346                            format: TextureFormatWrap(meta.format),
347                            mips: mips.iter().map(|data| CookedMip { data }).collect(),
348                        });
349                    })
350                    .depend_on(&compress_task);
351            }
352            PlainData::Hdr(data) => {
353                //TODO: compress as BC6E
354                //Note: we convert RGB32 to RGBA32 here, for now
355                assert_eq!(meta.format, blade_graphics::TextureFormat::Rgba32Float);
356                let in_texel_elements = data[0].len();
357                let out_texel_size = 4 * mem::size_of::<f32>();
358                let mut buf = vec![0u8; data.len() * out_texel_size];
359                for (slice, texel) in buf.chunks_mut(out_texel_size).zip(data) {
360                    unsafe {
361                        ptr::copy_nonoverlapping(
362                            texel.as_ptr(),
363                            slice.as_mut_ptr() as *mut f32,
364                            in_texel_elements,
365                        )
366                    }
367                }
368                cooker.finish(CookedImage {
369                    name: &[],
370                    extent: [src.width as u32, src.height as u32, 1],
371                    format: TextureFormatWrap(meta.format),
372                    mips: vec![CookedMip { data: &buf }],
373                });
374            }
375        }
376    }
377
378    fn serve(
379        &self,
380        image: CookedImage<'_>,
381        _exe_context: &choir::ExecutionContext,
382    ) -> Self::Output {
383        let name = str::from_utf8(image.name).unwrap();
384        let base_extent = blade_graphics::Extent {
385            width: image.extent[0],
386            height: image.extent[1],
387            depth: image.extent[2],
388        };
389        let texture = self
390            .gpu_context
391            .create_texture(blade_graphics::TextureDesc {
392                name,
393                format: image.format.0,
394                size: base_extent,
395                array_layer_count: 1,
396                mip_level_count: image.mips.len() as u32,
397                dimension: blade_graphics::TextureDimension::D2,
398                usage: blade_graphics::TextureUsage::COPY | blade_graphics::TextureUsage::RESOURCE,
399                sample_count: 1,
400            });
401        let view = self.gpu_context.create_texture_view(
402            texture,
403            blade_graphics::TextureViewDesc {
404                name,
405                format: image.format.0,
406                dimension: blade_graphics::ViewDimension::D2,
407                subresources: &Default::default(),
408            },
409        );
410        self.pending_operations
411            .lock()
412            .unwrap()
413            .initializations
414            .push(Initialization { dst: texture });
415
416        for (i, mip) in image.mips.iter().enumerate() {
417            let stage = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
418                name: &format!("{name}[{i}]/stage"),
419                size: mip.data.len() as u64,
420                memory: blade_graphics::Memory::Upload,
421            });
422            unsafe {
423                ptr::copy_nonoverlapping(mip.data.as_ptr(), stage.data(), mip.data.len());
424            }
425
426            let block_info = image.format.0.block_info();
427            let extent = base_extent.at_mip_level(i as u32);
428            let bytes_per_row = ((extent.width + block_info.dimensions.0 as u32 - 1)
429                / block_info.dimensions.0 as u32)
430                * block_info.size as u32;
431            let rows_per_image = (extent.height + block_info.dimensions.1 as u32 - 1)
432                / block_info.dimensions.1 as u32;
433            assert!(mip.data.len() >= rows_per_image as usize * bytes_per_row as usize,
434                "Image mip[{i}] data of size {} is insufficient for {bytes_per_row} bytes per {rows_per_image} rows",
435                mip.data.len());
436
437            let mut pending_ops = self.pending_operations.lock().unwrap();
438            pending_ops.transfers.push(Transfer {
439                stage,
440                bytes_per_row,
441                dst: texture,
442                extent,
443                mip_level: i as u32,
444            });
445        }
446
447        Texture {
448            object: texture,
449            view,
450            extent: base_extent,
451        }
452    }
453
454    fn delete(&self, texture: Self::Output) {
455        self.gpu_context.destroy_texture_view(texture.view);
456        self.gpu_context.destroy_texture(texture.object);
457    }
458}