use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::platform::Platform;
use crate::render::slang_source;
use crate::render::uniforms::{BINDLESS_POOL_SIZE, MAX_PROBES};
pub const VERTEX_MARKER: &str = "{SURFACE_VERTEX}";
pub const FRAGMENT_MARKER: &str = "{SURFACE_FRAGMENT}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Vertex,
Fragment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Program {
pub file: &'static str,
pub entry: &'static str,
pub stage: Stage,
}
pub const ALL: &[Program] = &[
Program {
file: "main_bindless.slang",
entry: "vertex_main_bindless",
stage: Stage::Vertex,
},
Program {
file: "main_bindless.slang",
entry: "fragment_main_bindless",
stage: Stage::Fragment,
},
];
pub fn program(entry: &str) -> Option<&'static Program> {
ALL.iter().find(|p| p.entry == entry)
}
pub fn programs(_platform: Platform) -> impl Iterator<Item = &'static Program> {
ALL.iter()
}
pub fn groups(platform: Platform) -> Vec<Vec<&'static Program>> {
if platform == Platform::Metal {
return alloc::vec![ALL.iter().collect()];
}
ALL.iter().map(|p| alloc::vec![p]).collect()
}
#[derive(Debug, Clone, Copy)]
pub struct Sources<'a> {
pub vertex: Option<&'a str>,
pub fragment: &'a str,
}
impl<'a> Sources<'a> {
pub fn splices(&self) -> Vec<(&'static str, &'a str)> {
let mut out = Vec::with_capacity(2);
if let Some(v) = self.vertex {
out.push((VERTEX_MARKER, v));
}
out.push((FRAGMENT_MARKER, self.fragment));
out
}
}
pub fn defines(
_program: &Program,
platform: Platform,
pool_size: usize,
probe_count: usize,
) -> Vec<(&'static str, String)> {
let probes = ("MAX_PROBES", MAX_PROBES.to_string());
match platform {
Platform::Metal => alloc::vec![
("METAL_ABI", "1".into()),
("POOL_SIZE", BINDLESS_POOL_SIZE.to_string()),
probes,
],
Platform::Hlsl => alloc::vec![("DXIL_ABI", "1".into()), probes],
Platform::Glsl => alloc::vec![
("POOL_SIZE", pool_size.to_string()),
("MAX_PROBES", probe_count.to_string()),
],
}
}
pub fn source_with(
program: &Program,
platform: Platform,
pool_size: usize,
probe_count: usize,
sources: &Sources<'_>,
resolve: impl Fn(&str) -> Option<&'static str>,
) -> String {
let defines = defines(program, platform, pool_size, probe_count);
let defines: Vec<(&str, &str)> = defines.iter().map(|(k, v)| (*k, v.as_str())).collect();
slang_source::assemble_with_splices(program.file, &defines, resolve, &sources.splices())
}
pub fn source(
program: &Program,
platform: Platform,
pool_size: usize,
probe_count: usize,
sources: &Sources<'_>,
) -> String {
source_with(
program,
platform,
pool_size,
probe_count,
sources,
crate::render::shaders::embedded,
)
}
#[cfg(test)]
mod tests {
use super::*;
const SHADE: &str =
"float4 shade(VertexOut in, GpuObjectData od) { return float4(1.0, 0.0, 1.0, 1.0); }";
fn fragment_only() -> Sources<'static> {
Sources {
vertex: None,
fragment: SHADE,
}
}
#[test]
fn a_grouping_covers_every_entry_of_the_host_once() {
for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
let mut grouped: Vec<&str> =
groups(platform).iter().flatten().map(|p| p.entry).collect();
grouped.sort_unstable();
let mut listed: Vec<&str> = programs(platform).map(|p| p.entry).collect();
listed.sort_unstable();
assert_eq!(grouped, listed, "{platform:?}");
}
}
#[test]
fn the_pair_is_the_whole_table_and_metal_groups_it() {
for platform in [Platform::Metal, Platform::Hlsl, Platform::Glsl] {
let entries: Vec<&str> = programs(platform).map(|p| p.entry).collect();
assert_eq!(
entries,
["vertex_main_bindless", "fragment_main_bindless"],
"{platform:?}"
);
}
let metal = groups(Platform::Metal);
assert_eq!(metal.len(), 1);
assert_eq!(metal[0].len(), 2);
for platform in [Platform::Hlsl, Platform::Glsl] {
assert_eq!(groups(platform).len(), 2, "{platform:?}");
assert!(
groups(platform).iter().all(|g| g.len() == 1),
"{platform:?}"
);
}
}
#[test]
fn every_entry_is_found_by_name_and_names_are_unique() {
for p in ALL {
assert_eq!(program(p.entry).map(|q| q.entry), Some(p.entry));
}
assert!(program("no_such_entry").is_none());
let mut names: Vec<&str> = ALL.iter().map(|p| p.entry).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), ALL.len());
}
#[test]
fn the_world_fragment_replaces_the_default_and_the_vertex_default_stays() {
let frag = program("fragment_main_bindless").unwrap();
let src = source(
frag,
Platform::Metal,
BINDLESS_POOL_SIZE,
MAX_PROBES,
&fragment_only(),
);
assert!(src.contains(SHADE));
assert!(
!src.contains("return shade_surface(in, od);"),
"default shade replaced"
);
assert!(!src.contains(FRAGMENT_MARKER) && !src.contains(VERTEX_MARKER));
assert!(src.contains("return project_vertex(model, pos, normal, tangent, color, uv);"));
let both = Sources {
vertex: Some(
"VertexOut transform(float4x4 m, float3 p, float3 n, float3 t, float3 c, float2 uv) { return project_vertex(m, p, n, t, c, uv); }",
),
fragment: SHADE,
};
let src = source(frag, Platform::Metal, BINDLESS_POOL_SIZE, MAX_PROBES, &both);
assert!(src.contains("VertexOut transform(float4x4 m,"));
assert!(!src.contains("return project_vertex(model, pos, normal, tangent, color, uv);"));
}
#[test]
fn the_pair_assembles_to_one_text() {
let vert = program("vertex_main_bindless").unwrap();
let frag = program("fragment_main_bindless").unwrap();
let a = source(
vert,
Platform::Metal,
BINDLESS_POOL_SIZE,
MAX_PROBES,
&fragment_only(),
);
let b = source(
frag,
Platform::Metal,
BINDLESS_POOL_SIZE,
MAX_PROBES,
&fragment_only(),
);
assert_eq!(a, b);
assert!(a.contains(SHADE));
assert!(a.contains("#define METAL_ABI 1"));
}
#[test]
fn defines_follow_the_host() {
let frag = program("fragment_main_bindless").unwrap();
let names =
|platform, pool| -> Vec<(&str, String)> { defines(frag, platform, pool, MAX_PROBES) };
assert_eq!(
names(Platform::Metal, 0),
[
("METAL_ABI", "1".to_string()),
("POOL_SIZE", "1024".to_string()),
("MAX_PROBES", "8".to_string())
]
);
assert_eq!(
names(Platform::Hlsl, 0),
[
("DXIL_ABI", "1".to_string()),
("MAX_PROBES", "8".to_string())
]
);
assert_eq!(
names(Platform::Glsl, 37),
[
("POOL_SIZE", "37".to_string()),
("MAX_PROBES", "8".to_string())
]
);
assert_eq!(
defines(frag, Platform::Glsl, 37, 4),
[
("POOL_SIZE", "37".to_string()),
("MAX_PROBES", "4".to_string())
]
);
assert_eq!(
defines(frag, Platform::Metal, 0, 4),
defines(frag, Platform::Metal, 0, MAX_PROBES)
);
}
}