use crate::ecs::Component;
use crate::ecs::PayloadLocator;
use crate::ecs::asset_id::AssetId;
use alloc::string::String;
use alloc::vec::Vec;
use super::compiled_programs::CompiledProgram;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ShaderStage {
Vertex,
Fragment,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Shader {
#[serde(skip)]
pub asset_id: AssetId,
pub fragment: String,
#[serde(default)]
pub vertex: Option<String>,
#[serde(skip)]
pub locator: Option<PayloadLocator>,
}
impl Shader {
pub fn stage(&self, stage: ShaderStage) -> Option<&str> {
match stage {
ShaderStage::Vertex => self.vertex.as_deref(),
ShaderStage::Fragment => Some(&self.fragment),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ShaderPrograms {
pub name: String,
pub vertex: Option<String>,
pub fragment: String,
pub programs: Vec<CompiledProgram>,
}
impl ShaderPrograms {
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 artifact(&self, entry: &str, digest: u64) -> Option<&[u8]> {
super::compiled_programs::artifact(&self.programs, entry, digest)
}
}
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;
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn a_shader_parses_from_authored_args() {
let s: Shader =
serde_json::from_str(r#"{"fragment":"assets/shaders/water.slang"}"#).unwrap();
assert_eq!(s.fragment, "assets/shaders/water.slang");
assert!(s.vertex.is_none(), "the vertex file is optional");
assert_eq!(s.stage(ShaderStage::Vertex), None);
assert_eq!(
s.stage(ShaderStage::Fragment),
Some("assets/shaders/water.slang")
);
assert_eq!(s.asset_id, AssetId::default());
assert!(s.locator.is_none());
let both: Shader =
serde_json::from_str(r#"{"vertex":"v.slang","fragment":"f.slang"}"#).unwrap();
assert_eq!(both.stage(ShaderStage::Vertex), Some("v.slang"));
let bytes = postcard::to_allocvec(&both).unwrap();
let back: Shader = postcard::from_bytes(&bytes).unwrap();
assert_eq!(back.vertex.as_deref(), Some("v.slang"));
assert_eq!(back.fragment, "f.slang");
}
#[test]
fn the_old_per_platform_table_is_rejected() {
let err = serde_json::from_str::<Shader>(
r#"{"vertex":{"sources":{"metal":"a.metal"}},"fragment":{"source":"a.metal"}}"#,
);
assert!(err.is_err());
}
#[test]
fn stages_parse_from_their_authored_spellings() {
let stage = |s: &str| serde_json::from_str::<ShaderStage>(s).unwrap();
assert_eq!(stage(r#""vertex""#), ShaderStage::Vertex);
assert_eq!(stage(r#""fragment""#), ShaderStage::Fragment);
}
#[test]
fn programs_round_trip_and_find_artifacts_by_entry_and_digest() {
let payload = ShaderPrograms {
name: "wall".to_string(),
vertex: None,
fragment: "float4 shade(VertexOut in, GpuObjectData od) { return 1.0; }".to_string(),
programs: vec![CompiledProgram {
entries: vec!["fragment_main".to_string()],
source_digest: 3,
artifact: vec![1, 2, 3],
}],
};
let bytes = payload.encode().expect("encode");
let decoded = ShaderPrograms::decode(&bytes).expect("decode");
assert_eq!(decoded, payload);
assert_eq!(decoded.artifact("fragment_main", 3), Some(&[1u8, 2, 3][..]));
assert_eq!(decoded.artifact("fragment_main", 4), None, "stale");
assert_eq!(decoded.artifact("vertex_main", 3), None);
}
#[test]
fn an_empty_payload_holds_no_programs() {
let payload = ShaderPrograms::default();
assert!(payload.programs.is_empty());
assert_eq!(
ShaderPrograms::decode(&payload.encode().unwrap()),
Ok(payload)
);
}
#[test]
fn decoding_garbage_is_an_error_not_a_panic() {
assert!(ShaderPrograms::decode(&[0xff, 0xff, 0xff]).is_err());
}
#[test]
fn a_shader_takes_its_identity_and_payload_on_load() {
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));
}
}