use crate::optics::raytracer::xyz_to_srgb_gamma;
use glam::Vec3;
#[cfg(target_arch = "wasm32")]
#[must_use]
const fn effective_thread_count(_threads: usize) -> usize {
1
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
fn effective_thread_count(threads: usize) -> usize {
if threads == 0 {
std::thread::available_parallelism().map_or(8, std::num::NonZero::get)
} else {
threads
}
}
fn tonemap_chunk(colors: &[Vec3], dst: &mut [u8], scale: f32) {
debug_assert_eq!(dst.len(), colors.len() * 4);
for (i, xyz) in colors.iter().enumerate() {
let rgba = xyz_to_srgb_gamma(*xyz * scale);
let p = i * 4;
dst[p] = rgba[0];
dst[p + 1] = rgba[1];
dst[p + 2] = rgba[2];
dst[p + 3] = rgba[3];
}
}
#[must_use]
pub fn tonemap_to_rgba_with_threads(colors: &[Vec3], scale: f32, threads: usize) -> Vec<u8> {
let mut out = vec![0u8; colors.len() * 4];
if colors.is_empty() {
return out;
}
let num_threads = effective_thread_count(threads).max(1);
let chunk_len = colors.len().div_ceil(num_threads).max(1);
if chunk_len >= colors.len() {
tonemap_chunk(colors, &mut out, scale);
return out;
}
std::thread::scope(|s| {
let color_chunks = colors.chunks(chunk_len);
let byte_chunks = out.chunks_mut(chunk_len * 4);
for (color_chunk, byte_chunk) in color_chunks.zip(byte_chunks) {
s.spawn(move || tonemap_chunk(color_chunk, byte_chunk, scale));
}
});
out
}
#[must_use]
pub fn tonemap_to_rgba(colors: &[Vec3], scale: f32) -> Vec<u8> {
tonemap_to_rgba_with_threads(colors, scale, 0)
}
#[cfg(test)]
mod tests {
use super::*;
struct Xorshift32(u32);
impl Xorshift32 {
const fn new(seed: u32) -> Self {
Self(if seed == 0 { 0xdead_beef } else { seed })
}
const fn next_u32(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
fn next_f32(&mut self) -> f32 {
(f64::from(self.next_u32()) / f64::from(u32::MAX)) as f32
}
}
fn irregular_colors(len: usize, seed: u32) -> Vec<Vec3> {
let mut rng = Xorshift32::new(seed);
(0..len)
.map(|_| {
Vec3::new(
rng.next_f32() * 4.0,
rng.next_f32() * 4.0,
rng.next_f32() * 4.0,
)
})
.collect()
}
#[test]
fn tonemap_is_thread_count_invariant() {
let colors = irregular_colors(10_007, 0x1234_5678);
let scale = 0.37;
let reference = tonemap_to_rgba_with_threads(&colors, scale, 1);
for threads in [2usize, 3, 8, 16, 200] {
let out = tonemap_to_rgba_with_threads(&colors, scale, threads);
assert_eq!(out, reference, "threads={threads}");
}
let auto = tonemap_to_rgba(&colors, scale);
assert_eq!(auto, reference, "auto thread count");
}
#[test]
fn matches_the_single_threaded_reference_loop() {
let colors = irregular_colors(2_503, 0xabcd_ef01);
let scale = 1.0;
let parallel = tonemap_to_rgba(&colors, scale);
let mut expected = vec![0u8; colors.len() * 4];
for (i, xyz) in colors.iter().enumerate() {
let rgba = xyz_to_srgb_gamma(*xyz * scale);
expected[i * 4] = rgba[0];
expected[i * 4 + 1] = rgba[1];
expected[i * 4 + 2] = rgba[2];
expected[i * 4 + 3] = rgba[3];
}
assert_eq!(parallel, expected);
}
#[test]
fn empty_input_does_not_panic() {
let colors: Vec<Vec3> = Vec::new();
assert_eq!(tonemap_to_rgba(&colors, 1.0), Vec::<u8>::new());
}
#[test]
fn single_pixel_does_not_panic() {
let colors = vec![Vec3::new(0.5, 0.5, 0.5)];
let out = tonemap_to_rgba_with_threads(&colors, 1.0, 8);
assert_eq!(out.len(), 4);
}
}