use std::borrow::Cow;
const FRAGMENTS: &[(&str, &str, &str)] = &[
(
"{POST_COMMON}",
"post_common.slang",
include_str!("shaders/post_common.slang"),
),
(
"{OBJECT_COMMON}",
"object_common.slang",
include_str!("shaders/object_common.slang"),
),
(
"{PROBE_TYPES}",
"probe_types.slang",
include_str!("shaders/probe_types.slang"),
),
(
"{PROBE_COMMON}",
"probe_common.slang",
include_str!("shaders/probe_common.slang"),
),
(
"{RT_TYPES}",
"rt_types.slang",
include_str!("shaders/rt_types.slang"),
),
(
"{RT_TRACE}",
"rt_trace.slang",
include_str!("shaders/rt_trace.slang"),
),
(
"{PARTICLE_TYPES}",
"particle_types.slang",
include_str!("shaders/particle_types.slang"),
),
];
pub(crate) fn assemble(
hot_reload: bool,
file: &str,
embedded: &'static str,
defines: &[(&str, &str)],
) -> String {
let mut spliced = read(hot_reload, file, embedded);
for (marker, fragment_file, fragment) in FRAGMENTS {
if spliced.contains(marker) {
let text = read(hot_reload, fragment_file, fragment);
spliced = Cow::Owned(spliced.replace(marker, &text));
}
}
concinnity_slang::inject_defines(&spliced, defines)
}
fn read(hot_reload: bool, file: &str, embedded: &'static str) -> Cow<'static, str> {
if !hot_reload {
return Cow::Borrowed(embedded);
}
let path = format!("{}/src/shaders/{}", env!("CARGO_MANIFEST_DIR"), file);
match std::fs::read_to_string(&path) {
Ok(s) => Cow::Owned(s),
Err(e) => {
tracing::debug!("hot-reload: falling back to embedded {file} ({e})");
Cow::Borrowed(embedded)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defines_lead_the_embedded_body() {
let src = assemble(
false,
"nonexistent.slang",
"BODY\n",
&[("A", "1"), ("B", "2")],
);
assert_eq!(src, "#define A 1\n#define B 2\nBODY\n");
}
#[test]
fn no_defines_is_the_body_verbatim() {
assert_eq!(assemble(false, "x.slang", "BODY\n", &[]), "BODY\n");
}
#[test]
fn a_missing_hot_reload_file_falls_back_to_embedded() {
let src = assemble(
true,
"definitely_not_a_shader.slang",
"BODY\n",
&[("A", "1")],
);
assert_eq!(src, "#define A 1\nBODY\n");
}
#[test]
fn the_post_common_marker_is_spliced_into_the_body() {
let src = assemble(false, "x.slang", "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_object_common_marker_is_spliced_into_the_body() {
let src = assemble(false, "x.slang", "A\n{OBJECT_COMMON}\nB\n", &[]);
assert!(!src.contains("{OBJECT_COMMON}"));
assert!(src.contains("struct GpuObjectData"));
assert!(src.contains("uint object_instance_index("));
assert!(src.contains("float3x3 normal_matrix("));
}
#[test]
fn every_marker_in_one_body_is_spliced() {
let src = assemble(false, "x.slang", "{POST_COMMON}\n{OBJECT_COMMON}\n", &[]);
assert!(src.contains("float2 combined_size("));
assert!(src.contains("struct GpuObjectData"));
}
#[test]
fn the_particle_types_marker_is_spliced_into_the_body() {
let src = assemble(false, "x.slang", "A\n{PARTICLE_TYPES}\nB\n", &[]);
assert!(!src.contains("{PARTICLE_TYPES}"));
assert!(src.contains("struct Particle"));
assert!(src.contains("struct ParticleParams"));
}
#[test]
fn the_paired_fragments_splice_records_before_helpers() {
let src = assemble(
false,
"x.slang",
"{PROBE_TYPES}\n{PROBE_COMMON}\n{RT_TYPES}\n{RT_TRACE}\n",
&[],
);
for marker in [
"{PROBE_TYPES}",
"{PROBE_COMMON}",
"{RT_TYPES}",
"{RT_TRACE}",
] {
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 a_body_without_the_marker_is_untouched() {
assert_eq!(assemble(false, "x.slang", "BODY\n", &[]), "BODY\n");
}
}