use glam::Vec3;
pub(crate) const FRESNEL_BRANCH_STREAM: u32 = 0x9E37_79B1;
pub(crate) const RUSSIAN_ROULETTE_STREAM: u32 = 0x517C_C1B7;
pub(crate) const BIREFRINGENT_SPLIT_STREAM: u32 = 0x2545_F491;
pub(crate) const MODE_COUPLING_STREAM: u32 = 0xCC9E_2D51;
pub(crate) const FROSTED_DIR_U_STREAM: u32 = 0x27D4_EB2F;
pub(crate) const FROSTED_DIR_V_STREAM: u32 = 0x1656_67B1;
pub(crate) const DISTANCE_SAMPLE_STREAM: u32 = 0xA24B_AED4;
pub(crate) const PHASE_DIR_U_STREAM: u32 = 0x9FB2_1C65;
pub(crate) const PHASE_DIR_V_STREAM: u32 = 0x1CE4_E5B9;
pub(crate) const NEE_ENV_DIR_U_STREAM: u32 = 0x4B72_5C19;
pub(crate) const NEE_ENV_DIR_V_STREAM: u32 = 0x2F1E_8A4D;
pub(crate) const FROSTED_NEE_ENV_DIR_U_STREAM: u32 = 0x6A09_E667;
pub(crate) const FROSTED_NEE_ENV_DIR_V_STREAM: u32 = 0xBB67_AE85;
#[must_use]
pub const fn hash_u32(mut x: u32) -> u32 {
x = x.wrapping_mul(0x85eb_ca6b);
x ^= x >> 13;
x = x.wrapping_mul(0xc2b2_ae35);
x ^= x >> 16;
x
}
pub const PIXEL_JITTER_X_ROTATION_STREAM: u32 = 0xA511_E9B3;
pub const PIXEL_JITTER_Y_ROTATION_STREAM: u32 = 0x63D8_1B23;
pub const HERO_WAVELENGTH_ROTATION_STREAM: u32 = 0x1B87_3593;
#[must_use]
#[inline]
pub fn low_discrepancy_base2(n: u32) -> f32 {
(n.reverse_bits() as f32) / 4_294_967_296.0
}
#[must_use]
#[inline]
pub fn radical_inverse_base(n: u32, base: u32) -> f32 {
let mut remaining = n;
let mut val = 0.0f32;
let mut inv_base = 1.0f32 / base as f32;
while remaining > 0 {
let digit = remaining % base;
val = (digit as f32).mul_add(inv_base, val);
inv_base /= base as f32;
remaining /= base;
}
val
}
#[must_use]
#[inline]
pub fn cranley_patterson_rotate(x: f32, offset: f32) -> f32 {
let sum = x + offset;
sum - sum.floor()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PixelRotations {
pub jitter_x: f32,
pub jitter_y: f32,
pub hero: f32,
}
#[must_use]
#[inline]
pub fn pixel_rotations(pixel: u32) -> PixelRotations {
PixelRotations {
jitter_x: low_discrepancy_base2(hash_u32(pixel ^ PIXEL_JITTER_X_ROTATION_STREAM)),
jitter_y: low_discrepancy_base2(hash_u32(pixel ^ PIXEL_JITTER_Y_ROTATION_STREAM)),
hero: low_discrepancy_base2(hash_u32(pixel ^ HERO_WAVELENGTH_ROTATION_STREAM)),
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SampleDraws {
pub seed: u32,
pub jitter_x: f32,
pub jitter_y: f32,
pub hero_rand: f32,
}
#[must_use]
#[inline]
pub fn sample_draws(pixel: u32, sample_num: u32, rot: &PixelRotations) -> SampleDraws {
let seed = hash_u32(pixel.wrapping_mul(0x9e37_79b9) ^ sample_num.wrapping_mul(0x85eb_ca6b));
let jitter_x = cranley_patterson_rotate(low_discrepancy_base2(sample_num), rot.jitter_x) - 0.5;
let jitter_y =
cranley_patterson_rotate(radical_inverse_base(sample_num, 3), rot.jitter_y) - 0.5;
let hero_rand = cranley_patterson_rotate(radical_inverse_base(sample_num, 5), rot.hero);
SampleDraws {
seed,
jitter_x,
jitter_y,
hero_rand,
}
}
#[inline]
pub fn add_finite_sample(sum: &mut Vec3, sample: Vec3) {
if sample.is_finite() {
*sum += sample;
}
}
#[cfg(test)]
mod non_finite_rule_tests {
use super::*;
#[test]
fn non_finite_samples_are_dropped_but_still_counted() {
let samples = [
Vec3::new(1.0, 2.0, 3.0),
Vec3::new(f32::NAN, 1.0, 1.0),
Vec3::new(4.0, 5.0, 6.0),
Vec3::new(1.0, f32::INFINITY, 1.0),
Vec3::new(1.0, 1.0, f32::NEG_INFINITY),
];
let mut sum = Vec3::ZERO;
for &s in &samples {
add_finite_sample(&mut sum, s);
}
let count = u8::try_from(samples.len()).expect("five samples");
assert_eq!(sum, Vec3::new(5.0, 7.0, 9.0));
let mean = sum / f32::from(count);
assert!(mean.is_finite(), "mean must stay finite, got {mean}");
assert_eq!(mean, Vec3::new(5.0, 7.0, 9.0) / 5.0);
}
#[test]
fn a_merged_pixel_stays_finite_when_one_backend_produced_nan_and_another_inf() {
let finite = Vec3::new(0.25, 0.5, 0.75);
let backend_a = [finite, Vec3::splat(f32::NAN), finite, finite];
let backend_b = [finite, finite, Vec3::new(f32::INFINITY, 0.0, 0.0), finite];
let mut sum_a = Vec3::ZERO;
for &s in &backend_a {
add_finite_sample(&mut sum_a, s);
}
let mut sum_b = Vec3::ZERO;
for &s in &backend_b {
add_finite_sample(&mut sum_b, s);
}
let merged = sum_a + sum_b;
let divisor = u8::try_from(backend_a.len() + backend_b.len()).expect("eight samples");
assert!(
merged.is_finite(),
"merged sum must stay finite, got {merged}"
);
assert_eq!(merged, finite * 6.0);
assert_eq!(divisor, 8, "dropped samples still count toward the divisor");
assert_eq!(merged / f32::from(divisor), finite * 6.0 / 8.0);
}
#[test]
fn an_all_non_finite_pixel_sums_to_zero() {
let mut sum = Vec3::ZERO;
add_finite_sample(&mut sum, Vec3::splat(f32::NAN));
add_finite_sample(&mut sum, Vec3::splat(f32::INFINITY));
add_finite_sample(&mut sum, Vec3::splat(f32::NEG_INFINITY));
assert_eq!(sum, Vec3::ZERO);
}
}
#[cfg(test)]
mod sample_draws_tests {
use super::*;
#[test]
fn draws_lie_in_their_unit_intervals() {
for pixel in 0..200u32 {
let rot = pixel_rotations(pixel);
for value in [rot.jitter_x, rot.jitter_y, rot.hero] {
assert!(
(0.0..1.0).contains(&value),
"pixel={pixel} rotation={value}"
);
}
for sample_num in 0..64u32 {
let d = sample_draws(pixel, sample_num, &rot);
assert!(
(-0.5..0.5).contains(&d.jitter_x),
"pixel={pixel} sample={sample_num} jitter_x={}",
d.jitter_x
);
assert!(
(-0.5..0.5).contains(&d.jitter_y),
"pixel={pixel} sample={sample_num} jitter_y={}",
d.jitter_y
);
assert!(
(0.0..1.0).contains(&d.hero_rand),
"pixel={pixel} sample={sample_num} hero_rand={}",
d.hero_rand
);
}
}
}
#[test]
fn distinct_pixel_or_sample_gives_a_distinct_seed() {
let mut by_sample = std::collections::HashSet::new();
let rot = pixel_rotations(7);
for sample_num in 0..512u32 {
assert!(
by_sample.insert(sample_draws(7, sample_num, &rot).seed),
"sample {sample_num} repeated a seed"
);
}
let mut by_pixel = std::collections::HashSet::new();
for pixel in 0..512u32 {
let rot = pixel_rotations(pixel);
assert!(
by_pixel.insert(sample_draws(pixel, 3, &rot).seed),
"pixel {pixel} repeated a seed"
);
}
}
#[test]
fn draws_are_bit_stable_across_calls() {
for pixel in [0u32, 1, 977, 123_456] {
for sample_num in [0u32, 1, 2, 31, 4096] {
let first = sample_draws(pixel, sample_num, &pixel_rotations(pixel));
let second = sample_draws(pixel, sample_num, &pixel_rotations(pixel));
assert_eq!(first.seed, second.seed);
assert_eq!(first.jitter_x.to_bits(), second.jitter_x.to_bits());
assert_eq!(first.jitter_y.to_bits(), second.jitter_y.to_bits());
assert_eq!(first.hero_rand.to_bits(), second.hero_rand.to_bits());
}
}
}
#[test]
fn hand_computed_draws_for_small_sample_indices() {
let zero = PixelRotations {
jitter_x: 0.0,
jitter_y: 0.0,
hero: 0.0,
};
let d0 = sample_draws(0, 0, &zero);
assert_eq!(d0.seed, 0);
assert_eq!(d0.jitter_x, -0.5);
assert_eq!(d0.jitter_y, -0.5);
assert_eq!(d0.hero_rand, 0.0);
let d2 = sample_draws(0, 2, &zero);
assert_eq!(d2.jitter_x, -0.25);
assert!(
(d2.jitter_y - (2.0 / 3.0 - 0.5)).abs() < 1e-6,
"{}",
d2.jitter_y
);
assert!((d2.hero_rand - 0.4).abs() < 1e-6, "{}", d2.hero_rand);
let rotated = PixelRotations {
jitter_x: 0.75,
jitter_y: 0.0,
hero: 0.5,
};
let d1 = sample_draws(0, 1, &rotated);
assert_eq!(d1.jitter_x, -0.25);
assert!(
(d1.jitter_y - (1.0 / 3.0 - 0.5)).abs() < 1e-6,
"{}",
d1.jitter_y
);
assert!((d1.hero_rand - 0.7).abs() < 1e-6, "{}", d1.hero_rand);
}
}
#[cfg(test)]
mod rng_decorrelation_tests {
use super::*;
#[test]
fn hashed_branch_draw_is_not_quantized_to_1000_values() {
let bounce = 0u32;
let mut values = std::collections::HashSet::new();
for seed in 0..5000u32 {
let v = hash_u32(seed ^ hash_u32(bounce ^ FRESNEL_BRANCH_STREAM));
values.insert(v);
}
assert!(
values.len() > 4900,
"hashed draw should yield close to 5000 distinct values across 5000 seeds (got {})",
values.len()
);
}
#[test]
fn fresnel_and_russian_roulette_streams_are_decorrelated() {
let mut agreements = 0u32;
let trials = 2000u32;
for seed in 0..trials {
let fresnel = hash_u32(seed ^ hash_u32(3u32 ^ FRESNEL_BRANCH_STREAM));
let rr = hash_u32(seed ^ hash_u32(3u32 ^ RUSSIAN_ROULETTE_STREAM));
if fresnel == rr {
agreements += 1;
}
}
assert!(
agreements == 0,
"the Fresnel-branch and Russian-roulette streams must not collide across {trials} trials (got {agreements} collisions)"
);
}
#[test]
fn russian_roulette_survival_is_unbiased_in_expectation() {
let q = 0.3f32; let trials = 200_000u32;
let mut total = 0.0f32;
for bounce in 0..trials {
let rr_rand = (hash_u32(bounce ^ hash_u32(bounce ^ RUSSIAN_ROULETTE_STREAM)) as f32)
/ 4_294_967_295.0;
if rr_rand <= q {
total += 1.0 / q;
}
}
let mean = total / trials as f32;
assert!(
(mean - 1.0).abs() < 0.02,
"weighted Russian-roulette survival should be unbiased (E[indicator/q] ~= 1.0), got {mean}"
);
}
}