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,
}
}
#[cfg(test)]
mod sample_draws_tests {
use super::*;
fn old_formula(pixel: u32, sample_num: u32) -> (u32, f32, f32, f32) {
let seed = hash_u32(pixel.wrapping_mul(0x9e37_79b9) ^ sample_num.wrapping_mul(0x85eb_ca6b));
let rot_jx = low_discrepancy_base2(hash_u32(pixel ^ PIXEL_JITTER_X_ROTATION_STREAM));
let rot_jy = low_discrepancy_base2(hash_u32(pixel ^ PIXEL_JITTER_Y_ROTATION_STREAM));
let rot_hero = low_discrepancy_base2(hash_u32(pixel ^ HERO_WAVELENGTH_ROTATION_STREAM));
let jx = cranley_patterson_rotate(low_discrepancy_base2(sample_num), rot_jx) - 0.5;
let jy = cranley_patterson_rotate(radical_inverse_base(sample_num, 3), rot_jy) - 0.5;
let hero_rand = cranley_patterson_rotate(radical_inverse_base(sample_num, 5), rot_hero);
(seed, jx, jy, hero_rand)
}
#[test]
fn sample_draws_matches_the_literal_old_formula_bit_for_bit() {
let mut checked = 0u32;
for pixel in 0..4000u32 {
let rot = pixel_rotations(pixel);
for sample_num in 0..3u32 {
let (seed, jx, jy, hero) = old_formula(pixel, sample_num);
let draws = sample_draws(pixel, sample_num, &rot);
assert_eq!(draws.seed, seed, "pixel={pixel} sample={sample_num}");
assert_eq!(draws.jitter_x, jx, "pixel={pixel} sample={sample_num}");
assert_eq!(draws.jitter_y, jy, "pixel={pixel} sample={sample_num}");
assert_eq!(draws.hero_rand, hero, "pixel={pixel} sample={sample_num}");
checked += 1;
}
}
assert!(
checked >= 3000,
"sanity: should have checked a few thousand pairs"
);
}
}
#[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}"
);
}
}