use std::{
fs,
path::{Path, PathBuf},
};
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn fnv1a_update(mut hash: u64, bytes: &[u8]) -> u64 {
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn collect_files(dir: &Path, root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files(&path, root, out);
} else if let Ok(rel) = path.strip_prefix(root) {
out.push(rel.to_path_buf());
}
}
}
fn normalize_path(rel: &Path) -> String {
rel.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
const TRANSPORT_PHYSICS_PIECES: &[&str] = &[
"01_prelude_polarization.wgsl",
"02_biaxial_eigenmodes.wgsl",
"03_dispersion_absorption_frosted.wgsl",
"04_scattering.wgsl",
"05_nee_env_sampling.wgsl",
"06_uniaxial_fullwave_complex.wgsl",
"07_uniaxial_fullwave_solve.wgsl",
"08_exit_splitting.wgsl",
"09_lighting_presets.wgsl",
"10_concave_tools.wgsl",
];
const TRANSPORT_BOUNCE_PIECES: &[&str] = &[
"01_header_and_constants.wgsl",
"02_scene_bindings.wgsl",
"03_rng_and_spectral_tables.wgsl",
"04_studio_and_hdr_environment.wgsl",
"05_intersect_and_edge_rounding.wgsl",
"06_nee_sampling.wgsl",
"07_camera_and_mode_selection.wgsl",
"07b_bounce_phases_types.wgsl",
"08a_index_setup.wgsl",
"08b_extend_scatter.wgsl",
"08c_surface.wgsl",
"08d_arms_frosted_tir_uni_entry.wgsl",
"08e_arms_uni_internal_generic.wgsl",
"08f_dispatch_split_tail_step.wgsl",
"09_finalize_and_ray_gen.wgsl",
];
const TRANSPORT_FUNCTIONS_PIECES: &[&str] = &[
"01_polarization_fresnel_tir.wgsl",
"02_dispersion_absorption_uniaxial_index.wgsl",
"03_frosted_and_scattering.wgsl",
"04_biaxial_and_scatter_extinguish.wgsl",
"05_uniaxial_fullwave_and_exit_splitting.wgsl",
"06_assigned_mode_and_balance.wgsl",
"07_env_distribution_sampling.wgsl",
"08_nee_and_intersect.wgsl",
];
const ZONING_PIECES: &[&str] = &[
"01_zone_table.wgsl",
"02_zone_kernel.wgsl",
"03_zone_absorption.wgsl",
];
const EW_KERNELS: &[(&str, &str)] = &[
("Generate", "ew_generate.wgsl"),
("Extend", "ew_extend.wgsl"),
("Scatter", "ew_scatter.wgsl"),
("NeeHg", "ew_nee_hg.wgsl"),
("Surface", "ew_surface.wgsl"),
("ArmFrosted", "ew_arm_frosted.wgsl"),
("ArmTir", "ew_arm_tir.wgsl"),
("ArmUniEntry", "ew_arm_uni_entry.wgsl"),
("ArmUniInternal", "ew_arm_uni_internal.wgsl"),
("ArmGeneric", "ew_arm_generic.wgsl"),
("SplitResolve", "ew_split_resolve.wgsl"),
("Tail", "ew_tail.wgsl"),
("Finalize", "ew_finalize.wgsl"),
];
const WHITTED_PIECES: &[&str] = &[
"00_bindings.wgsl",
"10_exp.wgsl",
"20_studio.wgsl",
"30_environment.wgsl",
"40_intersect.wgsl",
"50_chain.wgsl",
"60_frame.wgsl",
];
fn patch_once(text: &str, anchor: &str, replacement: &str, what: &str) -> String {
let count = text.matches(anchor).count();
assert!(
count == 1,
"zoning shader patch `{what}`: the anchor {anchor:?} occurs {count} times in the \
transport_bounce pieces (expected exactly 1); the default shader text moved, update \
build.rs"
);
text.replacen(anchor, replacement, 1)
}
fn zoned_bounce_shared(shaders_dir: &Path, bounce_shared: &str) -> String {
let with_table = patch_once(
bounce_shared,
"extraordinary_param_b: vec4<f32>,",
"extraordinary_param_b: vec4<f32>,\n // Zone table, appended at byte offset 576.\n zones: GpuZoneTable,",
"GpuGemMaterial.zones",
);
let with_call = patch_once(
&with_table,
"if (material.scattering_sigma_s <= 0.0) {",
"if (material.zones.header.zone_count > 0u && material.scattering_sigma_s <= 0.0) {\n \
// Zoned stone: per-zone absorption of this segment.\n \
zoned_interior_absorption(\n \
hit.t, (*current_origin), (*current_dir), wave_dir_at_bounce, lambdas,\n \
is_anisotropic, is_biaxial, c_axis, n_alpha_hero, n_beta_hero, n_gamma_hero,\n \
biax_ax0, biax_ax1, biax_ax2, n_o_hero_seed, n_e_hero_seed, (*is_extraordinary),\n \
alpha_o_hoisted, alpha_e_hoisted, alpha_beta_hoisted, stokes,\n \
);\n \
} else if (material.scattering_sigma_s <= 0.0) {",
"zoned interior absorption branch",
);
let mut out = with_call;
out.push_str("\n// ---- zoning units (shaders/zoning/*.wgsl) ----\n");
for piece in ZONING_PIECES {
let text = fs::read_to_string(shaders_dir.join("zoning").join(piece))
.unwrap_or_else(|e| panic!("failed to read shaders/zoning/{piece}: {e}"));
out.push_str(&text);
out.push('\n');
}
out
}
fn generate_transport_shaders(manifest_dir: &Path, out_dir: &Path) {
let shaders_dir = manifest_dir.join("src").join("renderer").join("shaders");
let read = |name: &str| -> String {
fs::read_to_string(shaders_dir.join(name))
.unwrap_or_else(|e| panic!("failed to read shaders/{name}: {e}"))
};
let read_concat = |subdir: &str, pieces: &[&str]| -> String {
pieces
.iter()
.map(|piece| {
let rel = format!("{subdir}/{piece}");
fs::read_to_string(shaders_dir.join(subdir).join(piece))
.unwrap_or_else(|e| panic!("failed to read shaders/{rel}: {e}"))
})
.collect()
};
let prelude = read_concat("transport_physics", TRANSPORT_PHYSICS_PIECES);
let mut whitted_tools = read("transport_physics/10_concave_tools.wgsl");
for (name, ty) in [
("t", "CtTool"),
("r", "CtInterval"),
("p", "CtPiece"),
("out", "CtSd"),
("s", "CtScan"),
("st", "CtCarve"),
] {
whitted_tools = whitted_tools.replace(
&format!("var {name}: {ty};"),
&format!("var {name}: {ty} = {ty}();"),
);
}
let whitted = format!(
"{}\n{}",
whitted_tools,
read_concat("whitted", WHITTED_PIECES)
);
fs::write(out_dir.join("whitted.generated.wgsl"), &whitted)
.expect("write whitted generated shader");
let whitted_hash = fnv1a_update(FNV_OFFSET_BASIS, whitted.replace("\r\n", "\n").as_bytes());
fs::write(
out_dir.join("whitted_kernel_hash.rs"),
format!("pub const WHITTED_KERNEL_HASH: &str = \"{whitted_hash:016x}\";\n"),
)
.expect("write whitted kernel hash");
let bounce_shared = read_concat("transport_bounce", TRANSPORT_BOUNCE_PIECES);
let bounce_shared = zoned_bounce_shared(&shaders_dir, &bounce_shared);
let units: [(&str, bool); 3] = [
("spectral_transport.wgsl", true),
("transport_functions.wgsl", false),
("wavefront_transport.wgsl", true),
];
for (name, needs_bounce_shared) in units {
let body = if name == "transport_functions.wgsl" {
read_concat("transport_functions", TRANSPORT_FUNCTIONS_PIECES)
} else {
read(name)
};
let generated = if needs_bounce_shared {
format!(
"// GENERATED by build.rs: shaders/transport_physics.wgsl + \
shaders/transport_bounce.wgsl + shaders/{name}.\n\
// Do not edit this file -- edit the sources above instead.\n\n\
{prelude}\n{bounce_shared}\n{body}"
)
} else {
format!(
"// GENERATED by build.rs: shaders/transport_physics.wgsl + shaders/{name}.\n\
// Do not edit this file -- edit the two sources above instead.\n\n{prelude}\n{body}"
)
};
let out_name = name.replace(".wgsl", ".generated.wgsl");
fs::write(out_dir.join(&out_name), generated)
.unwrap_or_else(|e| panic!("failed to write {out_name} to OUT_DIR: {e}"));
}
let common = fs::read_to_string(shaders_dir.join("event_wavefront").join("00_common.wgsl"))
.unwrap_or_else(|e| panic!("failed to read shaders/event_wavefront/00_common.wgsl: {e}"));
let mut ew_hash = FNV_OFFSET_BASIS;
for &(_name, file) in EW_KERNELS {
let kernel_body = fs::read_to_string(shaders_dir.join("event_wavefront").join(file))
.unwrap_or_else(|e| panic!("failed to read shaders/event_wavefront/{file}: {e}"));
let generated = format!(
"// GENERATED by build.rs: shaders/transport_physics.wgsl + \
shaders/transport_bounce.wgsl + shaders/event_wavefront/00_common.wgsl + \
shaders/event_wavefront/{file}.\n\
// Do not edit this file -- edit the sources above instead.\n\n\
{prelude}\n{bounce_shared}\n{common}\n{kernel_body}"
);
let out_name = file.replace(".wgsl", ".generated.wgsl");
fs::write(out_dir.join(&out_name), &generated)
.unwrap_or_else(|e| panic!("failed to write {out_name} to OUT_DIR: {e}"));
let normalized = generated.replace("\r\n", "\n");
ew_hash = fnv1a_update(ew_hash, normalized.as_bytes());
}
let hash_file_content = format!("pub const KERNEL_SET_HASH: &str = \"{ew_hash:016x}\";\n");
fs::write(out_dir.join("ew_kernel_set_hash.rs"), hash_file_content)
.unwrap_or_else(|e| panic!("failed to write ew_kernel_set_hash.rs to OUT_DIR: {e}"));
}
fn main() {
let manifest_dir = PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"),
);
let src_dir = manifest_dir.join("src");
let cargo_toml = manifest_dir.join("Cargo.toml");
let out_dir =
PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo for build scripts"));
generate_transport_shaders(&manifest_dir, &out_dir);
println!(
"cargo:rerun-if-changed={}",
src_dir.join("renderer/shaders/whitted").display()
);
let ew_shaders_dir = manifest_dir
.join("src")
.join("renderer")
.join("shaders")
.join("event_wavefront");
println!("cargo:rerun-if-changed={}", ew_shaders_dir.display());
println!(
"cargo:rerun-if-changed={}",
ew_shaders_dir.join("00_common.wgsl").display()
);
for &(_, file) in EW_KERNELS {
println!(
"cargo:rerun-if-changed={}",
ew_shaders_dir.join(file).display()
);
}
let mut rel_paths = Vec::new();
collect_files(&src_dir, &manifest_dir, &mut rel_paths);
rel_paths.retain(|p| {
p.extension()
.is_some_and(|ext| ext == "rs" || ext == "wgsl")
});
if rel_paths.is_empty() {
println!("cargo:rerun-if-changed={}", src_dir.display());
println!("cargo:rustc-env=INDICATRIX_SOURCE_HASH=unknown");
println!("cargo:rustc-env=INDICATRIX_BUILD_ID=unknown");
return;
}
rel_paths.push(PathBuf::from("Cargo.toml"));
rel_paths.sort();
let mut hash = FNV_OFFSET_BASIS;
for rel in &rel_paths {
let abs = manifest_dir.join(rel);
let Ok(contents) = fs::read(&abs) else {
continue;
};
let normalized: Vec<u8> = {
let mut out = Vec::with_capacity(contents.len());
let mut i = 0;
while i < contents.len() {
if contents[i] == b'\r' && contents.get(i + 1) == Some(&b'\n') {
} else {
out.push(contents[i]);
}
i += 1;
}
out
};
let rel_str = normalize_path(rel);
hash = fnv1a_update(hash, rel_str.as_bytes());
hash = fnv1a_update(hash, &[0u8]); hash = fnv1a_update(hash, &normalized);
println!("cargo:rerun-if-changed={}", abs.display());
}
println!("cargo:rerun-if-changed={}", src_dir.display());
println!("cargo:rerun-if-changed={}", cargo_toml.display());
let source_hash = format!("{hash:016x}");
println!("cargo:rustc-env=INDICATRIX_SOURCE_HASH={source_hash}");
let build_id = match std::env::var("CARGO_PKG_VERSION") {
Ok(version) if !version.trim().is_empty() => {
format!(
"{:016x}",
fnv1a_update(FNV_OFFSET_BASIS, version.trim().as_bytes())
)
}
_ => "unknown".to_string(),
};
println!("cargo:rustc-env=INDICATRIX_BUILD_ID={build_id}");
}