use crate::ecs::Component;
use crate::ecs::PayloadLocator;
use crate::ecs::asset_id::AssetId;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ShaderKind {
#[default]
Vertex,
Fragment,
#[serde(rename = "vertex_instanced", alias = "vertexinstanced")]
VertexInstanced,
}
impl ShaderKind {
pub fn compile_kind(&self) -> &'static str {
match self {
ShaderKind::Vertex | ShaderKind::VertexInstanced => "vertex",
ShaderKind::Fragment => "fragment",
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct StageSource {
#[serde(default)]
pub source: String,
#[serde(default)]
pub sources: Option<BTreeMap<String, String>>,
}
impl StageSource {}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Shader {
#[serde(skip)]
pub asset_id: AssetId,
pub vertex: StageSource,
pub fragment: StageSource,
#[serde(default)]
pub vertex_instanced: Option<StageSource>,
#[serde(skip)]
pub locator: Option<PayloadLocator>,
}
impl Shader {
pub fn stage(&self, kind: ShaderKind) -> Option<&StageSource> {
match kind {
ShaderKind::Vertex => Some(&self.vertex),
ShaderKind::Fragment => Some(&self.fragment),
ShaderKind::VertexInstanced => self.vertex_instanced.as_ref(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ShaderPayload {
pub stages: Vec<(ShaderKind, Vec<u8>)>,
}
impl ShaderPayload {
pub fn encode(&self) -> Result<Vec<u8>, postcard::Error> {
postcard::to_allocvec(self)
}
pub fn decode(bytes: &[u8]) -> Result<Self, postcard::Error> {
postcard::from_bytes(bytes)
}
pub fn stage(&self, kind: ShaderKind) -> Option<&[u8]> {
self.stages
.iter()
.find(|(k, _)| *k == kind)
.map(|(_, b)| b.as_slice())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn payload_round_trips_and_indexes_by_kind() {
let payload = ShaderPayload {
stages: vec![
(ShaderKind::Vertex, vec![1, 2, 3]),
(ShaderKind::Fragment, vec![4, 5]),
],
};
let bytes = payload.encode().expect("encode");
let decoded = ShaderPayload::decode(&bytes).expect("decode");
assert_eq!(decoded, payload);
assert_eq!(decoded.stage(ShaderKind::Vertex), Some(&[1u8, 2, 3][..]));
assert_eq!(decoded.stage(ShaderKind::Fragment), Some(&[4u8, 5][..]));
assert_eq!(decoded.stage(ShaderKind::VertexInstanced), None);
}
#[test]
fn stage_lookup_covers_every_kind() {
let s = Shader::default();
assert!(s.stage(ShaderKind::Vertex).is_some());
assert!(s.stage(ShaderKind::Fragment).is_some());
assert!(s.stage(ShaderKind::VertexInstanced).is_none());
}
#[test]
fn the_instanced_vertex_stage_compiles_as_a_vertex_stage() {
assert_eq!(ShaderKind::Vertex.compile_kind(), "vertex");
assert_eq!(ShaderKind::VertexInstanced.compile_kind(), "vertex");
assert_eq!(ShaderKind::Fragment.compile_kind(), "fragment");
assert_eq!(ShaderKind::default(), ShaderKind::Vertex);
}
#[test]
fn stage_kinds_parse_from_their_authored_spellings() {
let kind = |s: &str| serde_json::from_str::<ShaderKind>(s).unwrap();
assert_eq!(kind(r#""vertex""#), ShaderKind::Vertex);
assert_eq!(kind(r#""fragment""#), ShaderKind::Fragment);
assert_eq!(kind(r#""vertex_instanced""#), ShaderKind::VertexInstanced);
assert_eq!(kind(r#""vertexinstanced""#), ShaderKind::VertexInstanced);
assert_eq!(
serde_json::to_string(&ShaderKind::VertexInstanced).unwrap(),
r#""vertex_instanced""#
);
}
#[test]
fn a_shader_parses_from_authored_args() {
let s: Shader = serde_json::from_str(
r#"{"vertex":{"sources":{"metal":"my.metal"}},"fragment":{"source":"my.metal"}}"#,
)
.unwrap();
assert_eq!(s.fragment.source, "my.metal");
assert_eq!(
s.vertex.sources.as_ref().expect("per-platform")["metal"],
"my.metal"
);
assert!(s.vertex_instanced.is_none());
assert_eq!(s.asset_id, AssetId::default());
assert!(s.locator.is_none());
let bytes = postcard::to_allocvec(&s).unwrap();
let back: Shader = postcard::from_bytes(&bytes).unwrap();
assert_eq!(back.fragment.source, "my.metal");
}
#[test]
fn an_empty_payload_has_no_stages() {
let payload = ShaderPayload::default();
assert!(payload.stages.is_empty());
assert_eq!(payload.stage(ShaderKind::Vertex), None);
assert_eq!(
ShaderPayload::decode(&payload.encode().unwrap()),
Ok(payload)
);
}
#[test]
fn decoding_garbage_is_an_error_not_a_panic() {
assert!(ShaderPayload::decode(&[0xff, 0xff, 0xff]).is_err());
}
}
pub trait StageSourceExt {
fn current_platform_source(&self) -> Option<String>;
}
impl StageSourceExt for StageSource {
fn current_platform_source(&self) -> Option<String> {
let platform = crate::platform::Platform::current();
if let Some(sources) = &self.sources
&& let Some(src) = sources.get(platform.key())
{
return Some(src.clone());
}
if self.source.is_empty() {
return None;
}
let ext = super::path_extension(&self.source).unwrap_or("");
if platform.accepts_ext(ext) {
Some(self.source.clone())
} else {
None
}
}
}
impl Component for Shader {
const NAME: &'static str = "Shader";
fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
Ok(crate::blob::decode_exact(bytes)?)
}
fn inject_locator(&mut self, locator: PayloadLocator) {
self.locator = Some(locator);
}
fn inject_name(&mut self, id: crate::ecs::asset_id::AssetId) {
self.asset_id = id;
}
}
pub fn platform_key() -> &'static str {
crate::platform::Platform::current().key()
}
#[cfg(test)]
mod runtime_tests {
use super::*;
use alloc::string::ToString;
#[test]
fn compile_kind_maps_each_stage() {
assert_eq!(ShaderKind::Vertex.compile_kind(), "vertex");
assert_eq!(ShaderKind::VertexInstanced.compile_kind(), "vertex");
assert_eq!(ShaderKind::Fragment.compile_kind(), "fragment");
assert_eq!(ShaderKind::default(), ShaderKind::Vertex);
}
#[test]
fn current_platform_source_resolves_for_any_backend() {
let stage = StageSource {
sources: Some(
[
("metal".to_string(), "v.metal".to_string()),
("hlsl".to_string(), "v.hlsl".to_string()),
("glsl".to_string(), "v.glsl".to_string()),
]
.into_iter()
.collect(),
),
..Default::default()
};
assert!(stage.current_platform_source().is_some());
}
#[test]
fn single_source_resolves_only_for_matching_extensions() {
let stage = StageSource {
source: "v.metal".to_string(),
sources: None,
};
let platform = crate::platform::Platform::current();
assert_eq!(
stage.current_platform_source().is_some(),
platform.accepts_ext("metal")
);
}
#[test]
fn a_bare_source_resolves_only_when_the_platform_accepts_its_extension() {
let bare = |source: &str| StageSource {
source: source.to_string(),
sources: None,
};
assert_eq!(bare("").current_platform_source(), None, "nothing declared");
assert_eq!(
bare("v.slang").current_platform_source(),
Some("v.slang".to_string())
);
let other = ["metal", "hlsl", "glsl"]
.into_iter()
.find(|ext| *ext != platform_key())
.expect("some other platform exists");
assert_eq!(
bare(&alloc::format!("v.{other}")).current_platform_source(),
None,
"a {other} source is not loadable here"
);
}
#[test]
fn the_platform_key_is_the_running_backends_own() {
assert!(["metal", "hlsl", "glsl"].contains(&platform_key()));
assert_eq!(platform_key(), crate::platform::Platform::current().key());
}
#[test]
fn a_shader_takes_its_identity_and_payload_on_load() {
use crate::ecs::Component;
use crate::ecs::asset_id::AssetId;
let bytes = postcard::to_allocvec(&Shader::default()).expect("a shader encodes");
let mut shader = <Shader as Component>::from_baked(&bytes).expect("it loads back");
assert_eq!(Shader::NAME, "Shader");
shader.inject_name(AssetId(4));
assert_eq!(shader.asset_id, AssetId(4));
let locator = PayloadLocator {
blob_index: 1,
offset: 8,
len: 16,
};
shader.inject_locator(locator.clone());
assert_eq!(shader.locator, Some(locator));
}
}