1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
use std::{collections::HashMap, rc::Rc};

use ispc::WeightCollection;

mod ispc;

#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Format {
    Rgb8Unorm,
    Rgb8Snorm,
    Srgb8,
    Rgba8Unorm,
    Rgba8Snorm,
    Srgba8,
}

impl Format {
    pub fn num_channels(&self) -> usize {
        match self {
            Self::Rgb8Unorm | Self::Rgb8Snorm | Self::Srgb8 => 3,
            Self::Rgba8Unorm | Self::Rgba8Snorm | Self::Srgba8 => 4,
        }
    }

    pub fn pixel_size(&self) -> usize {
        self.channel_size_in_bytes() * self.num_channels()
    }

    pub fn channel_size_in_bytes(&self) -> usize {
        match self {
            Format::Rgb8Unorm
            | Format::Rgb8Snorm
            | Format::Srgb8
            | Format::Rgba8Unorm
            | Format::Rgba8Snorm
            | Format::Srgba8 => 1,
        }
    }
}

impl From<Format> for ispc::downsample_ispc::PixelFormat {
    fn from(value: Format) -> Self {
        match value {
            Format::Rgb8Unorm => ispc::PixelFormat_Rgb8Unorm,
            Format::Rgb8Snorm => ispc::PixelFormat_Rgb8Snorm,
            Format::Srgb8 => ispc::PixelFormat_Rgb8Unorm,
            Format::Rgba8Unorm => ispc::PixelFormat_Rgba8Unorm,
            Format::Rgba8Snorm => ispc::PixelFormat_Rgba8Snorm,
            Format::Srgba8 => ispc::PixelFormat_Rgba8Unorm,
        }
    }
}

#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum NormalMapFormat {
    R8g8b8,
    R8g8TangentSpaceReconstructedZ,
}

impl NormalMapFormat {
    pub fn pixel_size(self) -> usize {
        match self {
            NormalMapFormat::R8g8b8 => 3,
            NormalMapFormat::R8g8TangentSpaceReconstructedZ => 2,
        }
    }

    pub fn channel_size_in_bytes(self) -> usize {
        match self {
            Self::R8g8b8 | Self::R8g8TangentSpaceReconstructedZ => 1,
        }
    }
}

impl From<NormalMapFormat> for ispc::NormalMapFormat {
    fn from(value: NormalMapFormat) -> ispc::NormalMapFormat {
        match value {
            NormalMapFormat::R8g8b8 => ispc::NormalMapFormat_R8g8b8,
            NormalMapFormat::R8g8TangentSpaceReconstructedZ => {
                ispc::NormalMapFormat_R8g8TangentSpaceReconstructedZ
            }
        }
    }
}

/// Describes a source image which can be used for [`downsample()`]
/// The pixel data is stored as a slice to avoid unnecessarily cloning it.
pub struct Image<'a> {
    pixels: &'a [u8],
    width: u32,
    height: u32,
}

impl<'a> Image<'a> {
    /// Creates a new source image from the given pixel data slice, dimensions and format.
    pub fn new(pixels: &'a [u8], width: u32, height: u32) -> Self {
        Self {
            pixels,
            width,
            height,
        }
    }
}

/// Scales the alpha to the downscaled texture to preserve the overall alpha coverage.
///
/// If alpha cutoff is specified, any alpha value above it is considered visible of
/// which the percentage of visible texels will be. Otherwise, visibility is considered
/// a linear sum of the alpha values instead and the source and target alpha coverage
/// are calculated the same way.
pub fn scale_alpha_to_original_coverage(
    src: &Image<'_>,
    downsampled: &Image<'_>,
    alpha_cutoff: Option<f32>,
    format: Format,
) -> Vec<u8> {
    assert!(
        matches!(format, Format::Rgba8Unorm | Format::Rgba8Snorm),
        "Cannot retain alpha coverage on image with no alpha channel"
    );
    let mut alpha_scaled_data = downsampled.pixels.to_vec();
    unsafe {
        ispc::downsample_ispc::scale_to_alpha_coverage(
            src.width,
            src.height,
            src.pixels.as_ptr(),
            downsampled.width,
            downsampled.height,
            alpha_scaled_data.as_mut_ptr(),
            alpha_cutoff
                .as_ref()
                .map_or(std::ptr::null(), |alpha_cutoff| alpha_cutoff),
        );
    }
    alpha_scaled_data
}
// Defines a line of weights. `coefficients` contains a weight for each pixel after `start`
#[derive(Debug, Clone)]
struct CachedWeight {
    pub start: u32,
    pub coefficients: Rc<Vec<f32>>,
}

pub(crate) fn calculate_weights(src: u32, target: u32, filter_scale: f32) -> Vec<CachedWeight> {
    assert!(
        src >= target,
        "Trying to use downsampler to upsample or perform an operation which will cause no changes"
    );
    // Every line of weights is based on the start and end of the line, and its "center" which has the biggest weight.
    // These weight lines follow a pattern, so we can skip calculating some of them by caching all different line we get.
    // For that purpose, we first determine the variables which define the line.
    let mut variables = vec![ispc::WeightDimensions::default(); target as usize];

    unsafe {
        ispc::downsample_ispc::calculate_weight_dimensions(
            filter_scale,
            src,
            target,
            variables.as_mut_ptr(),
        );
    }

    let image_scale = src as f32 / target as f32;

    let mut res = Vec::with_capacity(target as usize);

    // We cache the weights in a map so that we can reuse them as we need.
    // Half of the total number of weights seems like a good starting point to avoid unnecessary copies when resizing.
    let mut reuse_heap = HashMap::<_, Rc<Vec<f32>>>::with_capacity(target as usize / 2);

    for v in variables.iter() {
        let coefficient_count = (v.src_end - v.src_start + 1.0) as u32;
        // 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.
        // We use them to create a key based on which we reuse ones we've calculated previously.
        let reuse_key = (
            coefficient_count,
            (v.src_center - v.src_start).to_ne_bytes(),
        );

        let reused = reuse_heap.get(&reuse_key);

        // If there is already a weight line calculated for that key, we clone it since it's an `Rc`.
        // If there isn't, we calculate the weights and add them to the reuse heap.
        let coefficients = if let Some(coefficients) = reused {
            coefficients.clone()
        } else {
            let mut coefficients = vec![0.0; coefficient_count as usize];
            unsafe {
                ispc::downsample_ispc::calculate_weights_lanczos(
                    image_scale,
                    filter_scale,
                    v as *const _,
                    coefficients.as_mut_ptr(),
                );
            }
            let coefficients = Rc::new(coefficients);
            reuse_heap.insert(reuse_key, coefficients.clone());
            coefficients
        };

        let cached = CachedWeight {
            start: v.src_start as u32,
            coefficients,
        };

        res.push(cached);
    }

    res
}

/// Samples the provided image down to the specified width and height.
/// `target_width` and `target_height` are expected to be less than or equal to their `src` counter parts.
/// Will panic if the target dimensions are the same as the source image's.
///
/// For a more fine-tunable version of this function, see [downsample_with_custom_scale].
pub fn downsample(
    src: &Image<'_>,
    target_width: u32,
    target_height: u32,
    pixel_stride_in_bytes: usize,
    format: Format,
) -> Vec<u8> {
    downsample_with_custom_scale(
        src,
        target_width,
        target_height,
        3.0,
        pixel_stride_in_bytes,
        format,
    )
}

fn precompute_lanczos_weights(
    src_width: u32,
    src_height: u32,
    dst_width: u32,
    dst_height: u32,
    filter_scale: f32,
) -> ispc::Weights {
    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.");
    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");
    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");
    assert!(
        filter_scale > 0.0,
        "filter_scale must be more than 0.0 when downsampling."
    );

    // The weights are calculated per-axis, and are only based on the source and target dimensions of that axis.
    // Because of that, if both axes have the same source and target dimensions, they will have the same weights.
    let width_weights =
        WeightCollection::new(calculate_weights(src_width, dst_width, filter_scale));
    let height_weights = if src_width == src_height && dst_width == dst_height {
        width_weights.clone()
    } else {
        WeightCollection::new(calculate_weights(src_height, dst_height, filter_scale))
    };

    ispc::Weights::new(width_weights, height_weights)
}

/// Version of [downsample] which allows for a custom filter scale, thus trading between speed and final image quality.
///
/// `filter_scale` controls how many samples are made relative to the size ratio between the source and target resolutions.
/// 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.
///
/// As a guideline, a `filter_scale` of 3.0 preserves detail well.
/// A scale of 1.0 preserves is good if speed is necessary, but still preserves a decent amount of detail.
/// Anything below is even faster, although the loss of detail becomes clear.
pub fn downsample_with_custom_scale(
    src: &Image<'_>,
    target_width: u32,
    target_height: u32,
    filter_scale: f32,
    pixel_stride_in_bytes: usize,
    format: Format,
) -> Vec<u8> {
    assert!(format.pixel_size() <= pixel_stride_in_bytes, "The stride between the pixels cannot be lower than the minimum size of the pixel according to the pixel format.");

    let sample_weights = precompute_lanczos_weights(
        src.width,
        src.height,
        target_width,
        target_height,
        filter_scale,
    );

    // The new implementation needs a src_height * target_width intermediate buffer.
    let mut scratch_space =
        vec![0u8; (src.height * target_width * format.num_channels() as u32) as usize];

    let mut output =
        vec![0u8; (target_width * target_height * format.num_channels() as u32) as usize];

    unsafe {
        if format.num_channels() == 3 {
            ispc::downsample_ispc::resample_with_cached_weights_3(
                &ispc::SourceImage {
                    width: src.width,
                    height: src.height,
                    data: src.pixels.as_ptr(),
                    pixel_stride: pixel_stride_in_bytes as u32,
                },
                &mut ispc::DownsampledImage {
                    width: target_width,
                    height: target_height,
                    data: output.as_mut_ptr(),
                    pixel_stride: pixel_stride_in_bytes as u32,
                },
                ispc::PixelFormat::from(format),
                &mut ispc::DownsamplingContext {
                    weights: *sample_weights.ispc_representation(),
                    scratch_space: scratch_space.as_mut_ptr(),
                },
            );
        } else {
            ispc::downsample_ispc::resample_with_cached_weights_4(
                &ispc::SourceImage {
                    width: src.width,
                    height: src.height,
                    data: src.pixels.as_ptr(),
                    pixel_stride: pixel_stride_in_bytes as u32,
                },
                &mut ispc::DownsampledImage {
                    width: target_width,
                    height: target_height,
                    data: output.as_mut_ptr(),
                    pixel_stride: pixel_stride_in_bytes as u32,
                },
                ispc::PixelFormat::from(format),
                &mut ispc::DownsamplingContext {
                    weights: *sample_weights.ispc_representation(),
                    scratch_space: scratch_space.as_mut_ptr(),
                },
            );
        }
    }

    output
}

/// Downsamples an image that is meant to be used as a normal map.
/// Uses a box filter instead of a lanczos filter, and normalizes each pixel to preserve unit length for the normals after downsampling.
///
/// 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.
pub fn downsample_normal_map(
    src: &Image<'_>,
    target_width: u32,
    target_height: u32,
    pixel_stride_in_bytes: usize,
    normal_map_format: NormalMapFormat,
) -> Vec<u8> {
    assert!(normal_map_format.pixel_size() <= 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.");

    let mut data = vec![255u8; (target_width * target_height) as usize * pixel_stride_in_bytes];

    unsafe {
        ispc::downsample_normal_map(
            &ispc::SourceImage {
                width: src.width,
                height: src.height,
                data: src.pixels.as_ptr(),
                pixel_stride: pixel_stride_in_bytes as u32,
            },
            &mut ispc::DownsampledImage {
                width: target_width,
                height: target_height,
                data: data.as_mut_ptr(),
                pixel_stride: pixel_stride_in_bytes as u32,
            },
            ispc::NormalMapFormat::from(normal_map_format),
        );
    }

    data
}