ispc_downsampler/
lib.rs

1use std::{collections::HashMap, rc::Rc};
2
3use ispc::WeightCollection;
4
5mod ispc;
6
7pub trait ImagePixelFormat: Copy {
8    /// Returns the number of channels that an image of this format would have in memory.
9    /// For example, while a normal map of format [`NormalMapFormat::R8g8TangentSpaceReconstructedZ`] would still have 3 channels when sampled,
10    /// in memory it will have 2 channels.
11    fn num_channel_in_memory(self) -> usize;
12
13    /// Returns the size of a single value channel, in bytes.
14    fn channel_size_in_bytes(self) -> usize;
15
16    /// Returns the size in bytes of a single pixel.
17    /// Generally this will be equal to [`channel_size_in_bytes()`][Self::channel_size_in_bytes()] * [`num_channel_in_memory()`][Self::num_channel_in_memory()].
18    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
98/// Describes a source image which can be used for [`downsample()`]
99/// The pixel data is stored as a slice to avoid unnecessarily cloning it.
100pub 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    /// Creates a new source image from the given pixel data slice, dimensions and format.
110    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
132/// Scales the alpha to the downscaled texture to preserve the overall alpha coverage.
133///
134/// If alpha cutoff is specified, any alpha value above it is considered visible of
135/// which the percentage of visible texels will be. Otherwise, visibility is considered
136/// a linear sum of the alpha values instead and the source and target alpha coverage
137/// are calculated the same way.
138pub 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// Defines a line of weights. `coefficients` contains a weight for each pixel after `start`
167#[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    // Every line of weights is based on the start and end of the line, and its "center" which has the biggest weight.
179    // These weight lines follow a pattern, so we can skip calculating some of them by caching all different line we get.
180    // For that purpose, we first determine the variables which define the line.
181    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    // We cache the weights in a map so that we can reuse them as we need.
197    // Half of the total number of weights seems like a good starting point to avoid unnecessary copies when resizing.
198    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        // The unique values that define a collection of cached weights are how many pixels it includes and the distance from its start to its center.
203        // We use them to create a key based on which we reuse ones we've calculated previously.
204        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        // If there is already a weight line calculated for that key, we clone it since it's an `Rc`.
212        // If there isn't, we calculate the weights and add them to the reuse heap.
213        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
241/// Samples the provided image down to the specified width and height.
242/// `target_width` and `target_height` are expected to be less than or equal to their `src` counter parts.
243/// Will panic if the target dimensions are the same as the source image's.
244///
245/// For a more fine-tunable version of this function, see [downsample_with_custom_scale].
246pub 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    // The weights are calculated per-axis, and are only based on the source and target dimensions of that axis.
266    // Because of that, if both axes have the same source and target dimensions, they will have the same weights.
267    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
278/// Version of [downsample] which allows for a custom filter scale, thus trading between speed and final image quality.
279///
280/// `filter_scale` controls how many samples are made relative to the size ratio between the source and target resolutions.
281/// The higher the scale, the more detail is preserved, but the slower the downsampling is. Note that the effect on the detail becomes smaller the higher the scale is.
282///
283/// As a guideline, a `filter_scale` of 3.0 preserves detail well.
284/// A scale of 1.0 preserves is good if speed is necessary, but still preserves a decent amount of detail.
285/// Anything below is even faster, although the loss of detail becomes clear.
286pub 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    // The new implementation needs a src_height * target_width intermediate buffer.
303    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
359/// Downsamples an image that is meant to be used as a normal map.
360/// Uses a box filter instead of a lanczos filter, and normalizes each pixel to preserve unit length for the normals after downsampling.
361///
362/// Returns a `Vec` with the downsampled data. If `normal_map_format.pixel_size() < pixel_stride_in_bytes`, the `Vec` will contain more values than channels than the format has specified, with all pixels in them initialized to 255.
363pub 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}