use crate::optics::{
absorption::{AbsorptionBand, BandShape},
raytracer::FacetFinish,
};
use core::mem::offset_of;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniform {
pub view_proj_inv: [f32; 16],
pub camera_pos: [f32; 3],
pub frame_index: u32,
pub screen_width: u32,
pub screen_height: u32,
pub max_bounces: u32,
pub gem_material_id: u32,
pub c_axis: [f32; 3],
pub env_intensity: f32,
}
const _: () = {
assert!(offset_of!(CameraUniform, view_proj_inv) == 0);
assert!(offset_of!(CameraUniform, camera_pos) == 64);
assert!(offset_of!(CameraUniform, frame_index) == 76);
assert!(offset_of!(CameraUniform, screen_width) == 80);
assert!(offset_of!(CameraUniform, screen_height) == 84);
assert!(offset_of!(CameraUniform, max_bounces) == 88);
assert!(offset_of!(CameraUniform, gem_material_id) == 92);
assert!(offset_of!(CameraUniform, c_axis) == 96);
assert!(offset_of!(CameraUniform, env_intensity) == 108);
assert!(size_of::<CameraUniform>() == 112);
};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DispersionParams {
pub model_type: u32,
_pad_after_model_type: [u32; 3],
pub param_a: [f32; 4],
pub param_b: [f32; 4],
pub param_c: [f32; 4],
pub c_axis_and_birefringence: [f32; 4],
pub is_anisotropic: u32,
pub biaxial_delta_beta_alpha: f32,
pub has_biaxial_delta: u32,
_pad_tail: f32,
}
const _: () = {
assert!(offset_of!(DispersionParams, model_type) == 0);
assert!(offset_of!(DispersionParams, param_a) == 16);
assert!(offset_of!(DispersionParams, param_b) == 32);
assert!(offset_of!(DispersionParams, param_c) == 48);
assert!(offset_of!(DispersionParams, c_axis_and_birefringence) == 64);
assert!(offset_of!(DispersionParams, is_anisotropic) == 80);
assert!(offset_of!(DispersionParams, biaxial_delta_beta_alpha) == 84);
assert!(offset_of!(DispersionParams, has_biaxial_delta) == 88);
assert!(offset_of!(DispersionParams, _pad_tail) == 92);
assert!(size_of::<DispersionParams>() == 96);
};
pub mod dispersion_model_type {
pub const SELLMEIER1: u32 = 0;
pub const SELLMEIER3: u32 = 1;
pub const CAUCHY: u32 = 2;
}
pub const MAX_ABSORPTION_BANDS: usize = 8;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuAbsorptionBand {
pub center_nm: f32,
pub width_nm: f32,
pub peak: f32,
pub shape: u32,
}
const _: () = {
assert!(offset_of!(GpuAbsorptionBand, center_nm) == 0);
assert!(offset_of!(GpuAbsorptionBand, width_nm) == 4);
assert!(offset_of!(GpuAbsorptionBand, peak) == 8);
assert!(offset_of!(GpuAbsorptionBand, shape) == 12);
assert!(size_of::<GpuAbsorptionBand>() == 16);
};
pub mod band_shape {
pub const GAUSSIAN_WAVELENGTH: u32 = 0;
pub const GAUSSIAN_ENERGY: u32 = 1;
}
pub mod crystal_system {
pub const CUBIC: u32 = 0;
pub const TETRAGONAL: u32 = 1;
pub const HEXAGONAL: u32 = 2;
pub const TRIGONAL: u32 = 3;
pub const ORTHORHOMBIC: u32 = 4;
pub const MONOCLINIC: u32 = 5;
pub const TRICLINIC: u32 = 6;
}
pub mod optical_character {
pub const ISOTROPIC: u32 = 0;
pub const UNIAXIAL_POSITIVE: u32 = 1;
pub const UNIAXIAL_NEGATIVE: u32 = 2;
pub const BIAXIAL_POSITIVE: u32 = 3;
pub const BIAXIAL_NEGATIVE: u32 = 4;
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuGemMaterial {
pub dispersion: DispersionParams,
pub crystal_system: u32,
pub optical_character: u32,
pub is_pleochroic: u32,
pub o_ray_band_count: u32,
pub e_ray_band_count: u32,
pub o_ray_bands: [GpuAbsorptionBand; MAX_ABSORPTION_BANDS],
pub e_ray_bands: [GpuAbsorptionBand; MAX_ABSORPTION_BANDS],
pub scattering_sigma_s: f32,
pub scattering_g: f32,
pub edge_rounding_radius: f32,
pub has_beta_ray: u32,
pub beta_ray_band_count: u32,
pub beta_ray_bands: [GpuAbsorptionBand; MAX_ABSORPTION_BANDS],
pub absorption_path_scale: f32,
pub has_extraordinary_dispersion: u32,
pub extraordinary_model_type: u32,
_pad_before_extraordinary_params: [u32; 3],
pub extraordinary_param_a: [f32; 4],
pub extraordinary_param_b: [f32; 4],
}
const _: () = {
assert!(offset_of!(GpuGemMaterial, dispersion) == 0);
assert!(offset_of!(GpuGemMaterial, crystal_system) == 96);
assert!(offset_of!(GpuGemMaterial, optical_character) == 100);
assert!(offset_of!(GpuGemMaterial, is_pleochroic) == 104);
assert!(offset_of!(GpuGemMaterial, o_ray_band_count) == 108);
assert!(offset_of!(GpuGemMaterial, e_ray_band_count) == 112);
assert!(offset_of!(GpuGemMaterial, o_ray_bands) == 116);
assert!(offset_of!(GpuGemMaterial, e_ray_bands) == 244);
assert!(offset_of!(GpuGemMaterial, scattering_sigma_s) == 372);
assert!(offset_of!(GpuGemMaterial, scattering_g) == 376);
assert!(offset_of!(GpuGemMaterial, edge_rounding_radius) == 380);
assert!(offset_of!(GpuGemMaterial, has_beta_ray) == 384);
assert!(offset_of!(GpuGemMaterial, beta_ray_band_count) == 388);
assert!(offset_of!(GpuGemMaterial, beta_ray_bands) == 392);
assert!(offset_of!(GpuGemMaterial, absorption_path_scale) == 520);
assert!(offset_of!(GpuGemMaterial, has_extraordinary_dispersion) == 524);
assert!(offset_of!(GpuGemMaterial, extraordinary_model_type) == 528);
assert!(offset_of!(GpuGemMaterial, extraordinary_param_a) == 544);
assert!(offset_of!(GpuGemMaterial, extraordinary_param_b) == 560);
assert!(size_of::<GpuGemMaterial>() == 576);
};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuCameraParams {
pub origin: [f32; 3],
pub fov_tan: f32,
pub forward: [f32; 3],
pub width: f32,
pub right: [f32; 3],
pub height: f32,
pub up: [f32; 3],
pub num_samples: u32,
}
const _: () = {
assert!(offset_of!(GpuCameraParams, origin) == 0);
assert!(offset_of!(GpuCameraParams, fov_tan) == 12);
assert!(offset_of!(GpuCameraParams, forward) == 16);
assert!(offset_of!(GpuCameraParams, width) == 28);
assert!(offset_of!(GpuCameraParams, right) == 32);
assert!(offset_of!(GpuCameraParams, height) == 44);
assert!(offset_of!(GpuCameraParams, up) == 48);
assert!(offset_of!(GpuCameraParams, num_samples) == 60);
assert!(size_of::<GpuCameraParams>() == 64);
};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuRay {
pub origin: [f32; 3],
_pad0: f32,
pub dir: [f32; 3],
_pad1: f32,
}
impl GpuRay {
#[must_use]
pub const fn new(origin: [f32; 3], dir: [f32; 3]) -> Self {
Self {
origin,
_pad0: 0.0,
dir,
_pad1: 0.0,
}
}
}
const _: () = {
assert!(offset_of!(GpuRay, origin) == 0);
assert!(offset_of!(GpuRay, dir) == 16);
assert!(size_of::<GpuRay>() == 32);
};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuHitRecord {
pub t: f32,
pub facet_idx: i32,
pub hit: u32,
_pad0: u32,
pub normal: [f32; 3],
_pad1: f32,
}
impl GpuHitRecord {
#[must_use]
pub const fn miss() -> Self {
Self {
t: 0.0,
facet_idx: -1,
hit: 0,
_pad0: 0,
normal: [0.0, 0.0, 0.0],
_pad1: 0.0,
}
}
#[must_use]
pub const fn hit(t: f32, facet_idx: i32, normal: [f32; 3]) -> Self {
Self {
t,
facet_idx,
hit: 1,
_pad0: 0,
normal,
_pad1: 0.0,
}
}
}
const _: () = {
assert!(offset_of!(GpuHitRecord, t) == 0);
assert!(offset_of!(GpuHitRecord, facet_idx) == 4);
assert!(offset_of!(GpuHitRecord, hit) == 8);
assert!(offset_of!(GpuHitRecord, normal) == 16);
assert!(size_of::<GpuHitRecord>() == 32);
};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuTransportParams {
pub num_pixels: u32,
pub max_bounces: u32,
pub sample_offset: u32,
pub env_mode: u32,
pub l0: f32,
pub studio_temp_k: f32,
pub studio_spot_mult: f32,
pub studio_exposure: f32,
pub studio_light_yaw: f32,
pub studio_light_pitch: f32,
pub pixel_offset: u32,
pub write_debug_buffers: u32,
pub white_balance: [f32; 3],
pub studio_use_d65: u32,
}
pub mod transport_env_mode {
pub const UNIFORM_FURNACE: u32 = 0;
pub const STUDIO_RIG: u32 = 1;
pub const HDR_MAP: u32 = 2;
}
impl GpuTransportParams {
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "one parameter per GpuTransportParams field, in field order, because \
that field order is itself the #[repr(C)] layout spectral_transport.wgsl's \
uniform buffer binds against; bundling them into a context struct here \
would just re-introduce this exact struct one level removed"
)]
pub const fn new(
num_pixels: u32,
max_bounces: u32,
sample_offset: u32,
env_mode: u32,
l0: f32,
studio_temp_k: f32,
studio_spot_mult: f32,
studio_exposure: f32,
studio_light_yaw: f32,
studio_light_pitch: f32,
white_balance: [f32; 3],
) -> Self {
Self {
num_pixels,
max_bounces,
sample_offset,
env_mode,
l0,
studio_temp_k,
studio_spot_mult,
studio_exposure,
studio_light_yaw,
studio_light_pitch,
pixel_offset: 0,
write_debug_buffers: 1,
white_balance,
studio_use_d65: 0,
}
}
#[must_use]
pub const fn with_pixel_offset(mut self, pixel_offset: u32) -> Self {
self.pixel_offset = pixel_offset;
self
}
#[must_use]
pub const fn with_debug_buffers_disabled(mut self) -> Self {
self.write_debug_buffers = 0;
self
}
#[must_use]
pub const fn with_studio_use_d65(mut self, use_d65: bool) -> Self {
self.studio_use_d65 = if use_d65 { 1 } else { 0 };
self
}
}
const _: () = {
assert!(offset_of!(GpuTransportParams, num_pixels) == 0);
assert!(offset_of!(GpuTransportParams, max_bounces) == 4);
assert!(offset_of!(GpuTransportParams, sample_offset) == 8);
assert!(offset_of!(GpuTransportParams, env_mode) == 12);
assert!(offset_of!(GpuTransportParams, l0) == 16);
assert!(offset_of!(GpuTransportParams, studio_temp_k) == 20);
assert!(offset_of!(GpuTransportParams, studio_spot_mult) == 24);
assert!(offset_of!(GpuTransportParams, studio_exposure) == 28);
assert!(offset_of!(GpuTransportParams, studio_light_yaw) == 32);
assert!(offset_of!(GpuTransportParams, studio_light_pitch) == 36);
assert!(offset_of!(GpuTransportParams, white_balance) == 48);
assert!(offset_of!(GpuTransportParams, studio_use_d65) == 60);
assert!(size_of::<GpuTransportParams>() == 64);
};
impl GpuGemMaterial {
#[must_use]
pub fn encode(material: &crate::optics::materials::GemMaterial) -> Self {
use crate::optics::materials::{CrystalSystem, OpticalCharacter};
let crystal_system_val = match material.crystal_system {
CrystalSystem::Cubic => crystal_system::CUBIC,
CrystalSystem::Tetragonal => crystal_system::TETRAGONAL,
CrystalSystem::Hexagonal => crystal_system::HEXAGONAL,
CrystalSystem::Trigonal => crystal_system::TRIGONAL,
CrystalSystem::Orthorhombic => crystal_system::ORTHORHOMBIC,
CrystalSystem::Monoclinic => crystal_system::MONOCLINIC,
CrystalSystem::Triclinic => crystal_system::TRICLINIC,
};
let optical_character_val = match material.optical_character {
OpticalCharacter::Isotropic => optical_character::ISOTROPIC,
OpticalCharacter::UniaxialPositive => optical_character::UNIAXIAL_POSITIVE,
OpticalCharacter::UniaxialNegative => optical_character::UNIAXIAL_NEGATIVE,
OpticalCharacter::BiaxialPositive => optical_character::BIAXIAL_POSITIVE,
OpticalCharacter::BiaxialNegative => optical_character::BIAXIAL_NEGATIVE,
};
let (o_ray_bands, o_ray_band_count) = encode_bands(&material.absorption.o_ray);
let (e_ray_bands, e_ray_band_count) = encode_bands(&material.absorption.e_ray);
let (beta_ray_bands, beta_ray_band_count) = material
.absorption
.beta_ray
.as_deref()
.map_or_else(empty_bands, encode_bands);
let (
has_extraordinary_dispersion,
extraordinary_model_type,
extraordinary_param_a,
extraordinary_param_b,
) = material.uniaxial_extraordinary_dispersion.map_or(
(0u32, 0u32, [0.0f32; 4], [0.0f32; 4]),
|e_dispersion| {
let (model_type, param_a, param_b) = encode_dispersion_model(&e_dispersion);
(1u32, model_type, param_a, param_b)
},
);
Self {
dispersion: DispersionParams::encode(material),
crystal_system: crystal_system_val,
optical_character: optical_character_val,
is_pleochroic: u32::from(material.absorption.is_pleochroic),
o_ray_band_count,
e_ray_band_count,
o_ray_bands,
e_ray_bands,
scattering_sigma_s: material.scattering_sigma_s,
scattering_g: material.scattering_g,
edge_rounding_radius: material.edge_rounding_radius,
has_beta_ray: u32::from(material.absorption.beta_ray.is_some()),
beta_ray_band_count,
beta_ray_bands,
absorption_path_scale: material.absorption_path_scale,
has_extraordinary_dispersion,
extraordinary_model_type,
_pad_before_extraordinary_params: [0; 3],
extraordinary_param_a,
extraordinary_param_b,
}
}
}
#[must_use]
const fn encode_dispersion_model(
model: &crate::optics::dispersion::DispersionModel,
) -> (u32, [f32; 4], [f32; 4]) {
use crate::optics::dispersion::DispersionModel;
match *model {
DispersionModel::Sellmeier1 { b1, c1 } => (
dispersion_model_type::SELLMEIER1,
[b1, 0.0, 0.0, 0.0],
[c1, 0.0, 0.0, 0.0],
),
DispersionModel::Sellmeier3 { b, c } => (
dispersion_model_type::SELLMEIER3,
[b[0], b[1], b[2], 0.0],
[c[0], c[1], c[2], 0.0],
),
DispersionModel::Cauchy { a, b, c } => {
(dispersion_model_type::CAUCHY, [a, b, c, 0.0], [0.0; 4])
}
}
}
impl DispersionParams {
#[must_use]
fn encode(material: &crate::optics::materials::GemMaterial) -> Self {
let (model_type, param_a, param_b) = encode_dispersion_model(&material.dispersion);
let c_axis = material.c_axis;
Self {
model_type,
_pad_after_model_type: [0; 3],
param_a,
param_b,
param_c: [0.0; 4],
c_axis_and_birefringence: [c_axis.x, c_axis.y, c_axis.z, material.birefringence_delta],
is_anisotropic: u32::from(
material.crystal_system != crate::optics::materials::CrystalSystem::Cubic
&& material.birefringence_delta.abs() > 1e-4,
),
biaxial_delta_beta_alpha: material.biaxial_delta_beta_alpha.unwrap_or(0.0),
has_biaxial_delta: u32::from(material.biaxial_delta_beta_alpha.is_some()),
_pad_tail: 0.0,
}
}
}
const fn empty_bands() -> ([GpuAbsorptionBand; MAX_ABSORPTION_BANDS], u32) {
(
[GpuAbsorptionBand {
center_nm: 0.0,
width_nm: 1.0,
peak: 0.0,
shape: band_shape::GAUSSIAN_WAVELENGTH,
}; MAX_ABSORPTION_BANDS],
0,
)
}
fn encode_bands(bands: &[AbsorptionBand]) -> ([GpuAbsorptionBand; MAX_ABSORPTION_BANDS], u32) {
assert!(
bands.len() <= MAX_ABSORPTION_BANDS,
"material has {} absorption bands, exceeding MAX_ABSORPTION_BANDS ({MAX_ABSORPTION_BANDS})",
bands.len()
);
let mut out = [GpuAbsorptionBand {
center_nm: 0.0,
width_nm: 1.0,
peak: 0.0,
shape: band_shape::GAUSSIAN_WAVELENGTH,
}; MAX_ABSORPTION_BANDS];
for (slot, band) in out.iter_mut().zip(bands.iter()) {
*slot = GpuAbsorptionBand {
center_nm: band.center_nm,
width_nm: band.width_nm,
peak: band.peak,
shape: match band.shape {
BandShape::GaussianWavelength => band_shape::GAUSSIAN_WAVELENGTH,
BandShape::GaussianEnergy => band_shape::GAUSSIAN_ENERGY,
},
};
}
(out, bands.len() as u32)
}
pub mod facet_finish {
pub const POLISHED: u32 = 0;
pub const FROSTED: u32 = 1;
}
#[must_use]
pub fn encode_facet_finishes(finishes: &[FacetFinish], num_planes: usize) -> Vec<u32> {
(0..num_planes)
.map(|i| match finishes.get(i).copied().unwrap_or_default() {
FacetFinish::Polished => facet_finish::POLISHED,
FacetFinish::Frosted => facet_finish::FROSTED,
})
.collect()
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuWavefrontParams {
pub chunk_rays: u32,
pub active_count: u32,
pub bounce: u32,
pub workgroup_count: u32,
}
const _: () = {
assert!(offset_of!(GpuWavefrontParams, chunk_rays) == 0);
assert!(offset_of!(GpuWavefrontParams, active_count) == 4);
assert!(offset_of!(GpuWavefrontParams, bounce) == 8);
assert!(offset_of!(GpuWavefrontParams, workgroup_count) == 12);
assert!(size_of::<GpuWavefrontParams>() == 16);
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn struct_sizes_match_documented_wgsl_layout() {
assert_eq!(size_of::<CameraUniform>(), 112);
assert_eq!(size_of::<DispersionParams>(), 96);
assert_eq!(size_of::<GpuAbsorptionBand>(), 16);
assert_eq!(size_of::<GpuGemMaterial>(), 576);
assert_eq!(size_of::<GpuTransportParams>(), 64);
assert_eq!(size_of::<GpuWavefrontParams>(), 16);
}
#[test]
fn gpu_absorption_band_offsets_match_documented_wgsl_layout() {
assert_eq!(offset_of!(GpuAbsorptionBand, center_nm), 0);
assert_eq!(offset_of!(GpuAbsorptionBand, width_nm), 4);
assert_eq!(offset_of!(GpuAbsorptionBand, peak), 8);
assert_eq!(offset_of!(GpuAbsorptionBand, shape), 12);
}
#[test]
fn gpu_gem_material_band_array_offsets_match_documented_wgsl_layout() {
assert_eq!(offset_of!(GpuGemMaterial, o_ray_bands), 116);
assert_eq!(offset_of!(GpuGemMaterial, e_ray_bands), 244);
assert_eq!(offset_of!(GpuGemMaterial, scattering_sigma_s), 372);
assert_eq!(offset_of!(GpuGemMaterial, beta_ray_bands), 392);
assert_eq!(offset_of!(GpuGemMaterial, absorption_path_scale), 520);
}
#[test]
fn encode_bands_maps_shape_correctly() {
let bands = vec![
AbsorptionBand::new(550.0, 20.0, 2.0),
AbsorptionBand::energy(620.0, 200.0, 1.5),
];
let (encoded, count) = encode_bands(&bands);
assert_eq!(count, 2);
assert_eq!(encoded[0].shape, band_shape::GAUSSIAN_WAVELENGTH);
assert_eq!(encoded[1].shape, band_shape::GAUSSIAN_ENERGY);
assert_eq!(encoded[2].shape, band_shape::GAUSSIAN_WAVELENGTH);
let (empty, empty_count) = empty_bands();
assert_eq!(empty_count, 0);
assert!(
empty
.iter()
.all(|b| b.shape == band_shape::GAUSSIAN_WAVELENGTH)
);
}
#[test]
fn encode_facet_finishes_defaults_to_polished() {
assert_eq!(encode_facet_finishes(&[], 4), vec![0, 0, 0, 0]);
let finishes = vec![FacetFinish::Frosted, FacetFinish::Polished];
assert_eq!(
encode_facet_finishes(&finishes, 4),
vec![
facet_finish::FROSTED,
facet_finish::POLISHED,
facet_finish::POLISHED,
facet_finish::POLISHED,
]
);
}
}