1use std::{collections::HashMap, rc::Rc};
2
3use ispc::WeightCollection;
4
5mod ispc;
6
7pub trait ImagePixelFormat: Copy {
8 fn num_channel_in_memory(self) -> usize;
12
13 fn channel_size_in_bytes(self) -> usize;
15
16 fn pixel_size_in_bytes(self) -> usize {
19 self.channel_size_in_bytes() * self.num_channel_in_memory()
20 }
21}
22
23#[derive(Clone, Copy, Eq, PartialEq, Debug)]
24pub enum AlbedoFormat {
25 Rgb8Unorm,
26 Rgb8Snorm,
27 Srgb8,
28 Rgba8Unorm,
29 Rgba8Snorm,
30 Srgba8,
31}
32
33impl ImagePixelFormat for AlbedoFormat {
34 fn num_channel_in_memory(self) -> usize {
35 match self {
36 Self::Rgb8Unorm | Self::Rgb8Snorm | Self::Srgb8 => 3,
37 Self::Rgba8Unorm | Self::Rgba8Snorm | Self::Srgba8 => 4,
38 }
39 }
40
41 fn channel_size_in_bytes(self) -> usize {
42 match self {
43 AlbedoFormat::Rgb8Unorm
44 | AlbedoFormat::Rgb8Snorm
45 | AlbedoFormat::Srgb8
46 | AlbedoFormat::Rgba8Unorm
47 | AlbedoFormat::Rgba8Snorm
48 | AlbedoFormat::Srgba8 => 1,
49 }
50 }
51}
52
53impl From<AlbedoFormat> for ispc::downsample_ispc::PixelFormat {
54 fn from(value: AlbedoFormat) -> Self {
55 match value {
56 AlbedoFormat::Rgb8Unorm => ispc::PixelFormat_Rgb8Unorm,
57 AlbedoFormat::Rgb8Snorm => ispc::PixelFormat_Rgb8Snorm,
58 AlbedoFormat::Srgb8 => ispc::PixelFormat_Rgb8Unorm,
59 AlbedoFormat::Rgba8Unorm => ispc::PixelFormat_Rgba8Unorm,
60 AlbedoFormat::Rgba8Snorm => ispc::PixelFormat_Rgba8Snorm,
61 AlbedoFormat::Srgba8 => ispc::PixelFormat_Rgba8Unorm,
62 }
63 }
64}
65
66#[derive(Clone, Copy, Eq, PartialEq, Debug)]
67pub enum NormalMapFormat {
68 Rgb8,
69 Rg8TangentSpaceReconstructedZ,
70}
71
72impl ImagePixelFormat for NormalMapFormat {
73 fn num_channel_in_memory(self) -> usize {
74 match self {
75 NormalMapFormat::Rgb8 => 3,
76 NormalMapFormat::Rg8TangentSpaceReconstructedZ => 2,
77 }
78 }
79
80 fn channel_size_in_bytes(self) -> usize {
81 match self {
82 Self::Rgb8 | Self::Rg8TangentSpaceReconstructedZ => 1,
83 }
84 }
85}
86
87impl From<NormalMapFormat> for ispc::NormalMapFormat {
88 fn from(value: NormalMapFormat) -> ispc::NormalMapFormat {
89 match value {
90 NormalMapFormat::Rgb8 => ispc::NormalMapFormat_R8g8b8,
91 NormalMapFormat::Rg8TangentSpaceReconstructedZ => {
92 ispc::NormalMapFormat_R8g8TangentSpaceReconstructedZ
93 }
94 }
95 }
96}
97
98pub struct Image<'a, F: ImagePixelFormat> {
101 pixels: &'a [u8],
102 width: u32,
103 height: u32,
104 pixel_stride_in_bytes: usize,
105 format: F,
106}
107
108impl<'a, F: ImagePixelFormat> Image<'a, F> {
109 pub fn new(pixels: &'a [u8], width: u32, height: u32, format: F) -> Self {
111 let pixel_size = format.pixel_size_in_bytes();
112 Self::new_with_pixel_stride(pixels, width, height, format, pixel_size)
113 }
114
115 pub fn new_with_pixel_stride(
116 pixels: &'a [u8],
117 width: u32,
118 height: u32,
119 format: F,
120 pixel_stride_in_bytes: usize,
121 ) -> Self {
122 Self {
123 pixels,
124 width,
125 height,
126 pixel_stride_in_bytes,
127 format,
128 }
129 }
130}
131
132pub fn scale_alpha_to_original_coverage(
139 src: &Image<'_, AlbedoFormat>,
140 downsampled: &Image<'_, AlbedoFormat>,
141 alpha_cutoff: Option<f32>,
142) -> Vec<u8> {
143 assert!(
144 matches!(
145 src.format,
146 AlbedoFormat::Rgba8Unorm | AlbedoFormat::Rgba8Snorm
147 ),
148 "Cannot retain alpha coverage on image with no alpha channel"
149 );
150 let mut alpha_scaled_data = downsampled.pixels.to_vec();
151 unsafe {
152 ispc::downsample_ispc::scale_to_alpha_coverage(
153 src.width,
154 src.height,
155 src.pixels.as_ptr(),
156 downsampled.width,
157 downsampled.height,
158 alpha_scaled_data.as_mut_ptr(),
159 alpha_cutoff
160 .as_ref()
161 .map_or(std::ptr::null(), |alpha_cutoff| alpha_cutoff),
162 );
163 }
164 alpha_scaled_data
165}
166#[derive(Debug, Clone)]
168struct CachedWeight {
169 pub start: u32,
170 pub coefficients: Rc<Vec<f32>>,
171}
172
173pub(crate) fn calculate_weights(src: u32, target: u32, filter_scale: f32) -> Vec<CachedWeight> {
174 assert!(
175 src >= target,
176 "Trying to use downsampler to upsample or perform an operation which will cause no changes"
177 );
178 let mut variables = vec![ispc::WeightDimensions::default(); target as usize];
182
183 unsafe {
184 ispc::downsample_ispc::calculate_weight_dimensions(
185 filter_scale,
186 src,
187 target,
188 variables.as_mut_ptr(),
189 );
190 }
191
192 let image_scale = src as f32 / target as f32;
193
194 let mut res = Vec::with_capacity(target as usize);
195
196 let mut reuse_heap = HashMap::<_, Rc<Vec<f32>>>::with_capacity(target as usize / 2);
199
200 for v in variables.iter() {
201 let coefficient_count = (v.src_end - v.src_start + 1.0) as u32;
202 let reuse_key = (
205 coefficient_count,
206 (v.src_center - v.src_start).to_ne_bytes(),
207 );
208
209 let reused = reuse_heap.get(&reuse_key);
210
211 let coefficients = if let Some(coefficients) = reused {
214 coefficients.clone()
215 } else {
216 let mut coefficients = vec![0.0; coefficient_count as usize];
217 unsafe {
218 ispc::downsample_ispc::calculate_weights_lanczos(
219 image_scale,
220 filter_scale,
221 v as *const _,
222 coefficients.as_mut_ptr(),
223 );
224 }
225 let coefficients = Rc::new(coefficients);
226 reuse_heap.insert(reuse_key, coefficients.clone());
227 coefficients
228 };
229
230 let cached = CachedWeight {
231 start: v.src_start as u32,
232 coefficients,
233 };
234
235 res.push(cached);
236 }
237
238 res
239}
240
241pub fn downsample(src: &Image<'_, AlbedoFormat>, target_width: u32, target_height: u32) -> Vec<u8> {
247 downsample_with_custom_scale(src, target_width, target_height, 3.0)
248}
249
250fn precompute_lanczos_weights(
251 src_width: u32,
252 src_height: u32,
253 dst_width: u32,
254 dst_height: u32,
255 filter_scale: f32,
256) -> ispc::Weights {
257 assert!(src_width != dst_width || src_height != dst_height, "Trying to downsample to an image of the same resolution as the source image. This operation can be avoided.");
258 assert!(src_width >= dst_width, "The width of the source image is less than the target's width. You are trying to upsample rather than downsample");
259 assert!(src_height >= dst_height, "The height of the source image is less than the target's height. You are trying to upsample rather than downsample");
260 assert!(
261 filter_scale > 0.0,
262 "filter_scale must be more than 0.0 when downsampling."
263 );
264
265 let width_weights =
268 WeightCollection::new(calculate_weights(src_width, dst_width, filter_scale));
269 let height_weights = if src_width == src_height && dst_width == dst_height {
270 width_weights.clone()
271 } else {
272 WeightCollection::new(calculate_weights(src_height, dst_height, filter_scale))
273 };
274
275 ispc::Weights::new(width_weights, height_weights)
276}
277
278pub fn downsample_with_custom_scale(
287 src: &Image<'_, AlbedoFormat>,
288 target_width: u32,
289 target_height: u32,
290 filter_scale: f32,
291) -> Vec<u8> {
292 assert!(src.format.pixel_size_in_bytes() <= src.pixel_stride_in_bytes, "The stride between the pixels cannot be lower than the minimum size of the pixel according to the pixel format.");
293
294 let sample_weights = precompute_lanczos_weights(
295 src.width,
296 src.height,
297 target_width,
298 target_height,
299 filter_scale,
300 );
301
302 let mut scratch_space =
304 vec![0u8; (src.height * target_width * src.format.num_channel_in_memory() as u32) as usize];
305
306 let mut output = vec![
307 0u8;
308 (target_width * target_height * src.format.num_channel_in_memory() as u32)
309 as usize
310 ];
311
312 unsafe {
313 if src.format.num_channel_in_memory() == 3 {
314 ispc::downsample_ispc::resample_with_cached_weights_3(
315 &ispc::SourceImage {
316 width: src.width,
317 height: src.height,
318 data: src.pixels.as_ptr(),
319 pixel_stride: src.pixel_stride_in_bytes as u32,
320 },
321 &mut ispc::DownsampledImage {
322 width: target_width,
323 height: target_height,
324 data: output.as_mut_ptr(),
325 pixel_stride: src.pixel_stride_in_bytes as u32,
326 },
327 ispc::PixelFormat::from(src.format),
328 &mut ispc::DownsamplingContext {
329 weights: *sample_weights.ispc_representation(),
330 scratch_space: scratch_space.as_mut_ptr(),
331 },
332 );
333 } else {
334 ispc::downsample_ispc::resample_with_cached_weights_4(
335 &ispc::SourceImage {
336 width: src.width,
337 height: src.height,
338 data: src.pixels.as_ptr(),
339 pixel_stride: src.pixel_stride_in_bytes as u32,
340 },
341 &mut ispc::DownsampledImage {
342 width: target_width,
343 height: target_height,
344 data: output.as_mut_ptr(),
345 pixel_stride: src.pixel_stride_in_bytes as u32,
346 },
347 ispc::PixelFormat::from(src.format),
348 &mut ispc::DownsamplingContext {
349 weights: *sample_weights.ispc_representation(),
350 scratch_space: scratch_space.as_mut_ptr(),
351 },
352 );
353 }
354 }
355
356 output
357}
358
359pub fn downsample_normal_map(
364 src: &Image<'_, NormalMapFormat>,
365 target_width: u32,
366 target_height: u32,
367) -> Vec<u8> {
368 assert!(src.format.pixel_size_in_bytes() <= src.pixel_stride_in_bytes, "The pixel stride in bytes must be more or equal than the size of a single pixel as described by the format of the normal map.");
369
370 let mut data = vec![255u8; (target_width * target_height) as usize * src.pixel_stride_in_bytes];
371
372 unsafe {
373 ispc::downsample_normal_map(
374 &ispc::SourceImage {
375 width: src.width,
376 height: src.height,
377 data: src.pixels.as_ptr(),
378 pixel_stride: src.pixel_stride_in_bytes as u32,
379 },
380 &mut ispc::DownsampledImage {
381 width: target_width,
382 height: target_height,
383 data: data.as_mut_ptr(),
384 pixel_stride: src.pixel_stride_in_bytes as u32,
385 },
386 ispc::NormalMapFormat::from(src.format),
387 );
388 }
389
390 data
391}