use crate::{
optics::raytracer::{
BIREFRINGENT_SPLIT_STREAM, FRESNEL_BRANCH_STREAM, FROSTED_DIR_U_STREAM,
FROSTED_DIR_V_STREAM, HERO_WAVELENGTH_ROTATION_STREAM, MODE_COUPLING_STREAM,
PIXEL_JITTER_X_ROTATION_STREAM, PIXEL_JITTER_Y_ROTATION_STREAM, RUSSIAN_ROULETTE_STREAM,
cranley_patterson_rotate, hash_u32, low_discrepancy_base2, radical_inverse_base,
wrapped_hero_wavelengths,
},
renderer::gpu::compute,
};
const SHADER_SRC: &str = include_str!("../shaders/rng_equivalence.wgsl");
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RngRecord {
pub seed: u32,
pub rot_jx_hash: u32,
pub rot_jy_hash: u32,
pub rot_hero_hash: u32,
pub sample_reversed: u32,
pub jx: f32,
pub jy: f32,
pub hero_rand: f32,
pub lambdas: [f32; 8],
pub fresnel_draws: [u32; 4],
pub rr_draws: [u32; 4],
pub biref_draws: [u32; 4],
pub mode_coupling_draws: [u32; 4],
pub frosted_dir_u_draws: [u32; 4],
pub frosted_dir_v_draws: [u32; 4],
}
const _: () = assert!(size_of::<RngRecord>() == 160);
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Params {
num_samples: u32,
num_bounces: u32,
_pad0: u32,
_pad1: u32,
}
pub const NUM_BOUNCES: u32 = 4;
#[must_use]
pub fn cpu_record(pixel: u32, sample: u32) -> RngRecord {
let seed = hash_u32(pixel.wrapping_mul(0x9e37_79b9) ^ sample.wrapping_mul(0x85eb_ca6b));
let rot_jx_hash = hash_u32(pixel ^ PIXEL_JITTER_X_ROTATION_STREAM);
let jitter_y_rot_hash = hash_u32(pixel ^ PIXEL_JITTER_Y_ROTATION_STREAM);
let rot_hero_hash = hash_u32(pixel ^ HERO_WAVELENGTH_ROTATION_STREAM);
let sample_reversed = sample.reverse_bits();
let rot_jx = low_discrepancy_base2(rot_jx_hash);
let jitter_y_rot = low_discrepancy_base2(jitter_y_rot_hash);
let rot_hero = low_discrepancy_base2(rot_hero_hash);
let jx = cranley_patterson_rotate(low_discrepancy_base2(sample), rot_jx) - 0.5;
let jy = cranley_patterson_rotate(radical_inverse_base(sample, 3), jitter_y_rot) - 0.5;
let hero_rand = cranley_patterson_rotate(radical_inverse_base(sample, 5), rot_hero);
let lambdas: [f32; 8] = wrapped_hero_wavelengths(hero_rand);
let mut fresnel_draws = [0u32; 4];
let mut rr_draws = [0u32; 4];
let mut biref_draws = [0u32; 4];
let mut mode_coupling_draws = [0u32; 4];
let mut frosted_dir_u_draws = [0u32; 4];
let mut frosted_v_draws = [0u32; 4];
for bounce in 0..NUM_BOUNCES {
let b = bounce;
fresnel_draws[bounce as usize] = hash_u32(seed ^ hash_u32(b ^ FRESNEL_BRANCH_STREAM));
rr_draws[bounce as usize] = hash_u32(seed ^ hash_u32(b ^ RUSSIAN_ROULETTE_STREAM));
biref_draws[bounce as usize] = hash_u32(seed ^ hash_u32(b ^ BIREFRINGENT_SPLIT_STREAM));
mode_coupling_draws[bounce as usize] = hash_u32(seed ^ hash_u32(b ^ MODE_COUPLING_STREAM));
frosted_dir_u_draws[bounce as usize] = hash_u32(seed ^ hash_u32(b ^ FROSTED_DIR_U_STREAM));
frosted_v_draws[bounce as usize] = hash_u32(seed ^ hash_u32(b ^ FROSTED_DIR_V_STREAM));
}
RngRecord {
seed,
rot_jx_hash,
rot_jy_hash: jitter_y_rot_hash,
rot_hero_hash,
sample_reversed,
jx,
jy,
hero_rand,
lambdas,
fresnel_draws,
rr_draws,
biref_draws,
mode_coupling_draws,
frosted_dir_u_draws,
frosted_dir_v_draws: frosted_v_draws,
}
}
#[derive(Debug, Clone)]
pub struct RngMismatch {
pub pixel: u32,
pub sample: u32,
pub field: &'static str,
pub cpu: String,
pub gpu: String,
}
#[derive(Debug, Clone)]
pub struct RngCheckResult {
pub total_records: usize,
pub mismatches: Vec<RngMismatch>,
pub float_ulp: FloatUlpResult,
}
impl RngCheckResult {
#[must_use]
pub const fn tier1_passed(&self) -> bool {
self.mismatches.is_empty()
}
#[must_use]
pub const fn passed(&self) -> bool {
self.tier1_passed() && self.float_ulp.passed()
}
}
fn compare_record(
pixel: u32,
sample: u32,
cpu: &RngRecord,
gpu: &RngRecord,
out: &mut Vec<RngMismatch>,
) {
macro_rules! check {
($field:ident, $name:literal) => {
if cpu.$field != gpu.$field {
out.push(RngMismatch {
pixel,
sample,
field: $name,
cpu: format!("{:?}", cpu.$field),
gpu: format!("{:?}", gpu.$field),
});
}
};
}
check!(seed, "seed");
check!(rot_jx_hash, "rot_jx_hash");
check!(rot_jy_hash, "rot_jy_hash");
check!(rot_hero_hash, "rot_hero_hash");
check!(sample_reversed, "sample_reversed");
check!(fresnel_draws, "fresnel_draws");
check!(rr_draws, "rr_draws");
check!(biref_draws, "biref_draws");
check!(mode_coupling_draws, "mode_coupling_draws");
check!(frosted_dir_u_draws, "frosted_dir_u_draws");
check!(frosted_dir_v_draws, "frosted_dir_v_draws");
}
pub const FLOAT_ULP_BUDGET: u32 = 4;
pub const FLOAT_ABS_FLOOR: f32 = 1e-4;
#[derive(Debug, Clone, Copy)]
pub struct FloatUlpArgmax {
pub pixel: u32,
pub sample: u32,
pub field: &'static str,
pub channel: usize,
pub cpu: f32,
pub gpu: f32,
pub ulp: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct FloatUlpResult {
pub budget: u32,
pub max_ulp: u32,
pub max_raw_ulp: u32,
pub argmax: Option<FloatUlpArgmax>,
pub over_budget_count: usize,
pub exempted_count: usize,
pub total_values_compared: usize,
}
impl FloatUlpResult {
#[must_use]
pub const fn passed(&self) -> bool {
self.over_budget_count == 0
}
}
#[derive(Debug, Default)]
struct FloatUlpAccumulator {
max_ulp: u32,
max_raw_ulp: u32,
argmax: Option<FloatUlpArgmax>,
over_budget_count: usize,
exempted_count: usize,
total_values_compared: usize,
}
impl FloatUlpAccumulator {
fn record_one(
&mut self,
pixel: u32,
sample: u32,
field: &'static str,
channel: usize,
c: f32,
g: f32,
) {
use crate::renderer::gpu::ulp::{ulp_distance, within_tolerance};
self.total_values_compared += 1;
let ulp = ulp_distance(c, g);
self.max_raw_ulp = self.max_raw_ulp.max(ulp);
if within_tolerance(c, g, FLOAT_ULP_BUDGET, FLOAT_ABS_FLOOR) {
if ulp > FLOAT_ULP_BUDGET {
self.exempted_count += 1;
}
return;
}
self.over_budget_count += 1;
if ulp > self.max_ulp {
self.max_ulp = ulp;
self.argmax = Some(FloatUlpArgmax {
pixel,
sample,
field,
channel,
cpu: c,
gpu: g,
ulp,
});
}
}
fn record(&mut self, pixel: u32, sample: u32, cpu: &RngRecord, gpu: &RngRecord) {
self.record_one(pixel, sample, "jx", 0, cpu.jx, gpu.jx);
self.record_one(pixel, sample, "jy", 0, cpu.jy, gpu.jy);
self.record_one(pixel, sample, "hero_rand", 0, cpu.hero_rand, gpu.hero_rand);
for (channel, (&c, &g)) in cpu.lambdas.iter().zip(gpu.lambdas.iter()).enumerate() {
self.record_one(pixel, sample, "lambdas", channel, c, g);
}
}
const fn finish(self) -> FloatUlpResult {
FloatUlpResult {
budget: FLOAT_ULP_BUDGET,
max_ulp: self.max_ulp,
max_raw_ulp: self.max_raw_ulp,
argmax: self.argmax,
over_budget_count: self.over_budget_count,
exempted_count: self.exempted_count,
total_values_compared: self.total_values_compared,
}
}
}
#[must_use]
pub fn run(
ctx: &crate::renderer::gpu::GpuContext,
num_pixels: u32,
num_samples: u32,
) -> RngCheckResult {
let total = (num_pixels as usize) * (num_samples as usize);
let params = Params {
num_samples,
num_bounces: NUM_BOUNCES,
_pad0: 0,
_pad1: 0,
};
let params_buf = compute::upload(
&ctx.device,
"rng_equivalence params",
std::slice::from_ref(¶ms),
wgpu::BufferUsages::UNIFORM,
);
let out_buf = compute::zeroed_buffer::<RngRecord>(
&ctx.device,
"rng_equivalence output",
total,
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
);
let pipeline =
compute::create_compute_pipeline(&ctx.device, "rng_equivalence", SHADER_SRC, "main");
let bind_group = compute::bind_buffers(
&ctx.device,
"rng_equivalence bind group",
&pipeline,
&[(0, ¶ms_buf), (1, &out_buf)],
);
let workgroups = u32::try_from(total).expect("a few samples").div_ceil(64);
compute::dispatch_and_wait(
&ctx.device,
&ctx.queue,
&pipeline,
&bind_group,
(workgroups, 1, 1),
);
let gpu_records: Vec<RngRecord> = compute::readback(&ctx.device, &ctx.queue, &out_buf, total);
let mut mismatches = Vec::new();
let mut float_acc = FloatUlpAccumulator::default();
for (idx, gpu_record) in gpu_records.iter().enumerate() {
let tuple = u32::try_from(idx).expect("pixels times samples is small");
let pixel = tuple / num_samples;
let sample = tuple % num_samples;
let cpu = cpu_record(pixel, sample);
if mismatches.len() < 64 {
compare_record(pixel, sample, &cpu, gpu_record, &mut mismatches);
}
float_acc.record(pixel, sample, &cpu, gpu_record);
}
RngCheckResult {
total_records: total,
mismatches,
float_ulp: float_acc.finish(),
}
}