use concinnity_core::components::ShaderPrograms;
use concinnity_core::platform::Platform;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::shader_programs::surface::{self, Sources};
use concinnity_core::render::shader_source;
use std::borrow::Cow;
#[derive(Clone, Copy)]
pub(crate) struct Request {
pub platform: Platform,
pub hot_reload: bool,
}
pub(crate) fn artifact<'a>(
programs: &'a ShaderPrograms,
entry: &str,
req: &Request,
compile: impl FnOnce(Platform, &str, &str, &str) -> RenderResult<Vec<u8>>,
) -> RenderResult<Cow<'a, [u8]>> {
let label = programs.name.as_str();
let program = surface::program(entry).ok_or_else(|| {
RenderError::ShaderCompile(format!(
"Shader '{label}': no main-pass entry named '{entry}'"
))
})?;
let source = source(program, req, &programs.sources());
let digest = shader_source::source_digest(&source);
if let Some(bytes) = programs.artifact(entry, digest) {
return Ok(Cow::Borrowed(bytes));
}
tracing::debug!("Shader '{label}': {entry} predates the engine template, compiling");
compile(req.platform, program.file, program.entry, &source)
.map(Cow::Owned)
.map_err(|e| e.context(format_args!("Shader '{label}': compiling '{entry}'")))
}
fn source(program: &surface::Program, req: &Request, sources: &Sources<'_>) -> String {
if !req.hot_reload {
return surface::source(program, req.platform, sources);
}
surface::source_with(
program,
req.platform,
sources,
crate::shader::source::from_checkout,
)
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::components::ShaderSource;
use concinnity_core::components::compiled_programs::CompiledProgram;
const SHADE: &str = "float4 shade(VertexOut v, GpuObjectData od) { return (float4)(1.0); }";
fn request(platform: Platform) -> Request {
Request {
platform,
hot_reload: false,
}
}
fn no_compile(_: Platform, _: &str, _: &str, _: &str) -> RenderResult<Vec<u8>> {
panic!("a matching artifact reached the compiler")
}
fn stored(platform: Platform, entry: &str, bytes: &[u8]) -> ShaderPrograms {
let mut programs = ShaderPrograms {
name: "wall".to_string(),
vertex: None,
fragment: ShaderSource {
path: "shaders/wall.hlsl".to_string(),
text: SHADE.to_string(),
},
programs: Vec::new(),
};
let program = surface::program(entry).unwrap();
let src = surface::source(program, platform, &programs.sources());
programs.programs.push(CompiledProgram {
entry: entry.to_string(),
source_digest: shader_source::source_digest(&src),
artifact: bytes.to_vec(),
});
programs
}
#[test]
fn a_matching_artifact_is_taken_without_compiling() {
for entry in ["vertex_main_bindless", "fragment_main_bindless"] {
let programs = stored(Platform::Metal, entry, b"stored bytes");
let got =
artifact(&programs, entry, &request(Platform::Metal), no_compile).expect("stored");
assert_eq!(got.as_ref(), b"stored bytes");
assert!(matches!(got, Cow::Borrowed(_)), "no compile was needed");
}
}
#[test]
fn an_artifact_from_another_host_does_not_match() {
let metal = stored(Platform::Metal, "fragment_main_bindless", b"stored bytes");
let program = surface::program("fragment_main_bindless").unwrap();
let other_host = surface::source(program, Platform::Vulkan, &metal.sources());
assert!(
metal
.artifact(
"fragment_main_bindless",
shader_source::source_digest(&other_host)
)
.is_none()
);
}
#[test]
fn an_unknown_entry_is_an_error() {
let programs = stored(Platform::Metal, "fragment_main_bindless", b"x");
let err = artifact(
&programs,
"no_such_entry",
&request(Platform::Metal),
no_compile,
)
.unwrap_err()
.to_string();
assert!(err.contains("no_such_entry"), "got: {err}");
}
#[test]
fn a_stale_artifact_compiles_its_entry_and_names_the_shader() {
let mut programs = stored(Platform::Metal, "fragment_main_bindless", b"stale");
programs.programs[0].source_digest ^= 1;
let program = surface::program("fragment_main_bindless").unwrap();
let want = surface::source(program, Platform::Metal, &programs.sources());
let req = request(Platform::Metal);
let got = artifact(
&programs,
"fragment_main_bindless",
&req,
|platform, file, entry, src| {
assert_eq!(platform, Platform::Metal);
assert_eq!(
(file, entry),
("main_bindless.hlsl", "fragment_main_bindless")
);
assert_eq!(src, want);
Ok(b"fresh".to_vec())
},
)
.expect("compiled");
assert_eq!(got.as_ref(), b"fresh");
let err = artifact(&programs, "fragment_main_bindless", &req, |_, _, _, _| {
Err(RenderError::ShaderCompile("no compiler".to_string()))
})
.unwrap_err()
.to_string();
assert!(err.contains("Shader 'wall'"), "got: {err}");
assert!(err.contains("fragment_main_bindless"), "got: {err}");
assert!(err.contains("no compiler"), "got: {err}");
}
}