use alloc::string::String;
use crate::platform::Platform;
use crate::render::slang_source;
pub const FILE: &str = "raymarch.slang";
pub const BODY_MARKER: &str = "{SDF_BODY}";
pub const SCENE_TAP: &str = "sampleSceneRefracted";
pub fn field_taps_scene(field: &str) -> bool {
field.contains(SCENE_TAP)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
Surface,
Volumetric,
Shadow,
}
impl Family {
pub fn define(self) -> &'static str {
match self {
Family::Surface => "RAYMARCH_SURFACE",
Family::Volumetric => "RAYMARCH_VOLUMETRIC",
Family::Shadow => "RAYMARCH_SHADOW",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Vertex,
Fragment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Program {
pub entry: &'static str,
pub stage: Stage,
pub family: Family,
}
pub const ALL: &[Program] = &[
Program {
entry: "raymarch_vertex",
stage: Stage::Vertex,
family: Family::Surface,
},
Program {
entry: "raymarch_fragment",
stage: Stage::Fragment,
family: Family::Surface,
},
Program {
entry: "raymarch_volumetric_vertex",
stage: Stage::Vertex,
family: Family::Volumetric,
},
Program {
entry: "raymarch_volumetric_fragment",
stage: Stage::Fragment,
family: Family::Volumetric,
},
Program {
entry: "raymarch_shadow_vertex",
stage: Stage::Vertex,
family: Family::Shadow,
},
Program {
entry: "raymarch_shadow_fragment",
stage: Stage::Fragment,
family: Family::Shadow,
},
];
pub fn abi_define(platform: Platform) -> Option<&'static str> {
match platform {
Platform::Metal => Some("RAYMARCH_METAL"),
Platform::Hlsl => Some("RAYMARCH_DXIL"),
Platform::Glsl => None,
}
}
pub fn families(volumetric: bool, cast_shadows: bool) -> impl Iterator<Item = Family> {
let own = if volumetric {
Family::Volumetric
} else {
Family::Surface
};
let shadow = (cast_shadows && !volumetric).then_some(Family::Shadow);
core::iter::once(own).chain(shadow)
}
pub fn programs(volumetric: bool, cast_shadows: bool) -> impl Iterator<Item = &'static Program> {
families(volumetric, cast_shadows).flat_map(|f| ALL.iter().filter(move |p| p.family == f))
}
pub fn defines(
family: Family,
platform: Platform,
) -> alloc::vec::Vec<(&'static str, &'static str)> {
let mut out = alloc::vec::Vec::with_capacity(2);
if let Some(abi) = abi_define(platform) {
out.push((abi, "1"));
}
out.push((family.define(), "1"));
out
}
pub fn source_with(
family: Family,
platform: Platform,
field: &str,
resolve: impl Fn(&str) -> Option<&'static str>,
) -> String {
slang_source::assemble_with_splices(
FILE,
&defines(family, platform),
resolve,
&[(BODY_MARKER, field)],
)
}
pub fn source(family: Family, platform: Platform, field: &str) -> String {
source_with(family, platform, field, crate::render::shaders::embedded)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
#[test]
fn a_surface_volume_compiles_its_own_pair_and_nothing_else() {
let entries: Vec<&str> = programs(false, false).map(|p| p.entry).collect();
assert_eq!(entries, ["raymarch_vertex", "raymarch_fragment"]);
}
#[test]
fn a_casting_surface_volume_adds_the_shadow_pair() {
let entries: Vec<&str> = programs(false, true).map(|p| p.entry).collect();
assert_eq!(
entries,
[
"raymarch_vertex",
"raymarch_fragment",
"raymarch_shadow_vertex",
"raymarch_shadow_fragment"
]
);
}
#[test]
fn a_volumetric_volume_never_compiles_a_shadow_caster() {
for cast_shadows in [false, true] {
let entries: Vec<&str> = programs(true, cast_shadows).map(|p| p.entry).collect();
assert_eq!(
entries,
["raymarch_volumetric_vertex", "raymarch_volumetric_fragment"]
);
}
}
#[test]
fn the_defines_name_the_abi_only_where_the_source_has_a_branch_for_it() {
assert_eq!(
defines(Family::Surface, Platform::Metal),
[("RAYMARCH_METAL", "1"), ("RAYMARCH_SURFACE", "1")]
);
assert_eq!(
defines(Family::Shadow, Platform::Hlsl),
[("RAYMARCH_DXIL", "1"), ("RAYMARCH_SHADOW", "1")]
);
assert_eq!(
defines(Family::Volumetric, Platform::Glsl),
[("RAYMARCH_VOLUMETRIC", "1")]
);
}
#[test]
fn every_entry_the_table_names_is_declared_in_the_source() {
let text = crate::render::shaders::embedded(FILE).expect("raymarch.slang");
for program in ALL {
assert!(
text.contains(program.entry),
"{} names no entry in {FILE}",
program.entry
);
}
assert!(text.contains(BODY_MARKER), "{FILE} carries no body marker");
}
#[test]
fn the_scene_tap_is_declared_by_the_helpers() {
let text =
crate::render::shaders::embedded("raymarch_common.slang").expect("raymarch_common");
assert!(text.contains(SCENE_TAP), "{SCENE_TAP} declares nothing");
}
#[test]
fn only_a_field_naming_the_tap_reads_as_tapping() {
let opaque = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
assert!(!field_taps_scene(opaque));
let refractive = "s.transmitted = sampleSceneRefracted(frag_uv, normal, 0.05);";
assert!(field_taps_scene(refractive));
}
#[test]
fn the_assembled_source_is_not_what_the_flag_reads() {
let opaque = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
let src = source(Family::Surface, Platform::Metal, opaque);
assert!(src.contains(SCENE_TAP), "the template declares the tap");
assert!(!field_taps_scene(opaque));
}
#[test]
fn the_field_is_spliced_and_the_hosts_assemble_differently() {
let field = "float map(float3 p, SdfParams q, float t) { return 1.0; }";
let mut seen = Vec::new();
for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
let src = source(Family::Surface, platform, field);
assert!(src.contains(field), "{platform:?} lost the field");
assert!(!src.contains(BODY_MARKER), "{platform:?} left the marker");
assert!(src.starts_with("#define "), "{platform:?} defines lead");
seen.push(slang_source::source_digest(&src));
}
seen.dedup();
assert_eq!(seen.len(), 3, "two hosts assemble identical source");
}
#[test]
fn two_fields_assemble_to_two_digests() {
let a = source(Family::Surface, Platform::Metal, "// one");
let b = source(Family::Surface, Platform::Metal, "// two");
assert_ne!(
slang_source::source_digest(&a),
slang_source::source_digest(&b)
);
}
}