use alloc::borrow::Cow;
use alloc::string::{String, ToString};
use crate::render::shaders;
pub const FRAGMENTS: &[(&str, &str)] = &[
("{POST_COMMON}", "post_common.slang"),
("{MAIN_TYPES}", "main_types.slang"),
("{OBJECT_COMMON}", "object_common.slang"),
("{PROBE_TYPES}", "probe_types.slang"),
("{PROBE_COMMON}", "probe_common.slang"),
("{RT_TYPES}", "rt_types.slang"),
("{RT_TRACE}", "rt_trace.slang"),
("{PARTICLE_TYPES}", "particle_types.slang"),
("{RAYMARCH_TYPES}", "raymarch_types.slang"),
("{LIGHT_TYPES}", "light_types.slang"),
("{RAYMARCH_COMMON}", "raymarch_common.slang"),
("{MAIN_SHADING}", "main_shading.slang"),
("{SHADOW_BIAS}", "shadow_bias.slang"),
("{SURFACE_VERTEX}", "surface_vertex_default.slang"),
("{SURFACE_FRAGMENT}", "surface_fragment_default.slang"),
];
pub fn inject_defines(source: &str, defines: &[(&str, &str)]) -> String {
if defines.is_empty() {
return source.to_string();
}
let mut out = String::with_capacity(source.len() + defines.len() * 32);
for (name, value) in defines {
out.push_str("#define ");
out.push_str(name);
out.push(' ');
out.push_str(value);
out.push('\n');
}
out.push_str(source);
out
}
pub fn assemble(file: &str, defines: &[(&str, &str)]) -> String {
assemble_with(file, defines, shaders::embedded)
}
pub fn source_digest(source: &str) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x1000_0000_01b3;
let mut hash = OFFSET;
for byte in source.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
pub fn assemble_with(
file: &str,
defines: &[(&str, &str)],
resolve: impl Fn(&str) -> Option<&'static str>,
) -> String {
assemble_with_splices(file, defines, resolve, &[])
}
pub fn assemble_with_splices(
file: &str,
defines: &[(&str, &str)],
resolve: impl Fn(&str) -> Option<&'static str>,
splices: &[(&str, &str)],
) -> String {
let mut spliced = read(file, &resolve);
for (marker, fragment_file) in FRAGMENTS {
if spliced.contains(marker) && !splices.iter().any(|(m, _)| m == marker) {
let text = read(fragment_file, &resolve);
spliced = Cow::Owned(spliced.replace(marker, &text));
}
}
spliced = splice_all(spliced, splices);
inject_defines(&spliced, defines)
}
fn splice_all<'a>(mut text: Cow<'a, str>, splices: &[(&str, &str)]) -> Cow<'a, str> {
for (marker, fill) in splices {
if text.contains(marker) {
text = Cow::Owned(text.replace(marker, fill));
}
}
text
}
fn read(file: &str, resolve: &impl Fn(&str) -> Option<&'static str>) -> Cow<'static, str> {
match resolve(file).or_else(|| shaders::embedded(file)) {
Some(text) => Cow::Borrowed(text),
None => Cow::Borrowed(""),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
#[test]
fn defines_lead_the_body() {
assert_eq!(
inject_defines("BODY\n", &[("A", "1"), ("B", "2")]),
"#define A 1\n#define B 2\nBODY\n"
);
assert_eq!(inject_defines("BODY\n", &[]), "BODY\n");
}
#[test]
fn the_post_common_marker_is_spliced_into_the_body() {
let src = assemble_with("x.slang", &[], |f| {
(f == "x.slang").then_some("A\n{POST_COMMON}\nB\n")
});
assert!(!src.contains("{POST_COMMON}"));
assert!(src.contains("float2 combined_size("));
assert!(src.starts_with("A\n") && src.ends_with("\nB\n"));
}
#[test]
fn the_paired_fragments_splice_records_before_helpers() {
let body = "{PROBE_TYPES}\n{PROBE_COMMON}\n{RT_TYPES}\n{RT_TRACE}\n";
let src = assemble_with("x.slang", &[], |f| (f == "x.slang").then_some(body));
for (marker, _) in FRAGMENTS {
assert!(!src.contains(marker), "unspliced {marker}");
}
assert!(src.find("struct ProbeSet") < src.find("float3 probe_set_specular("));
assert!(src.find("struct RtGeomEntry") < src.find("bool rt_trace_reflection("));
}
#[test]
fn the_resolver_overrides_the_embedded_copy_and_declining_falls_back() {
let overridden = assemble_with("fog.slang", &[], |f| {
(f == "fog.slang").then_some("OVERRIDDEN\n")
});
assert_eq!(overridden, "OVERRIDDEN\n");
assert_eq!(
assemble_with("fog.slang", &[], |_| None),
assemble("fog.slang", &[])
);
}
#[test]
fn a_caller_splice_fills_a_marker_the_table_does_not_name() {
let src = assemble_with_splices(
"x.slang",
&[],
|f| (f == "x.slang").then_some("A\n{SDF_BODY}\nB\n"),
&[("{SDF_BODY}", "float map() { return 1.0; }")],
);
assert_eq!(src, "A\nfloat map() { return 1.0; }\nB\n");
}
#[test]
fn a_caller_splice_reaches_a_marker_inside_a_shared_fragment() {
let src = assemble_with_splices(
"x.slang",
&[],
|f| match f {
"x.slang" => Some("{POST_COMMON}\n"),
"post_common.slang" => Some("frag {SDF_BODY} end"),
_ => None,
},
&[("{SDF_BODY}", "FILLED")],
);
assert_eq!(src, "frag FILLED end\n");
}
#[test]
fn a_caller_splice_is_not_rescanned_for_table_markers() {
let src = assemble_with_splices(
"x.slang",
&[],
|f| (f == "x.slang").then_some("{SURFACE_FRAGMENT}\n"),
&[(
"{SURFACE_FRAGMENT}",
"// see {MAIN_TYPES} and {SURFACE_VERTEX}\n",
)],
);
assert_eq!(src, "// see {MAIN_TYPES} and {SURFACE_VERTEX}\n\n");
}
#[test]
fn a_caller_splice_wins_over_a_table_row_for_the_same_marker() {
let body = "{SURFACE_VERTEX}\n{SURFACE_FRAGMENT}\n";
let src = assemble_with_splices(
"x.slang",
&[],
|f| (f == "x.slang").then_some(body),
&[("{SURFACE_FRAGMENT}", "WORLD SHADE")],
);
assert!(src.contains("WORLD SHADE"));
assert!(
!src.contains("float4 shade(VertexOut in, GpuObjectData od)\n{"),
"default shade replaced"
);
assert!(
src.contains("VertexOut transform("),
"default transform kept"
);
}
#[test]
fn the_raymarch_source_assembles_records_then_body_then_the_world_field() {
let src = assemble_with_splices(
"raymarch.slang",
&[("RAYMARCH_METAL", "1"), ("RAYMARCH_SURFACE", "1")],
shaders::embedded,
&[("{SDF_BODY}", "// the world's field")],
);
for (marker, _) in FRAGMENTS {
assert!(!src.contains(marker), "unspliced {marker}");
}
assert!(!src.contains("{SDF_BODY}"));
let types = src.find("struct SdfVolumeUniforms").expect("records");
let lights = src.find("struct LightUniforms").expect("light records");
let body = src.find("RayHit coneRaymarch(").expect("marcher");
let field = src.find("// the world's field").expect("world field");
let entry = src
.find("RaymarchFragOut raymarch_fragment(")
.expect("entry");
assert!(lights < types, "light records precede the volume block");
assert!(types < body, "records precede the body that reads them");
assert!(
body < field,
"the field is declared after the helpers call it"
);
assert!(field < entry, "the entry points come last");
}
#[test]
fn both_passes_take_the_light_records_from_one_fragment() {
for file in ["main_bindless.slang", "raymarch.slang"] {
let src = assemble(file, &[]);
assert!(!src.contains("{LIGHT_TYPES}"), "{file} left the marker");
assert_eq!(
src.matches("struct LightUniforms").count(),
1,
"{file} declares the light block other than once"
);
}
}
#[test]
fn a_body_without_the_marker_is_untouched() {
let src = assemble_with("x.slang", &[], |f| (f == "x.slang").then_some("BODY\n"));
assert_eq!(src, "BODY\n");
}
#[test]
fn every_fragment_the_table_names_is_embedded() {
for (marker, file) in FRAGMENTS {
assert!(
shaders::embedded(file).is_some(),
"{marker} names a missing {file}"
);
}
}
#[test]
fn the_source_table_is_a_unique_set_the_lookup_covers() {
let mut names: Vec<&str> = shaders::SOURCES.iter().map(|(n, _)| *n).collect();
names.sort_unstable();
let count = names.len();
names.dedup();
assert_eq!(names.len(), count, "duplicate shader name");
for (name, text) in shaders::SOURCES {
assert_eq!(shaders::embedded(name), Some(*text));
}
assert_eq!(shaders::embedded("not_a_shader.slang"), None);
}
}