Skip to main content

concinnity_core/components/
shader.rs

1//! Runtime behavior for the Shader asset. The authored schema (Shader,
2//! StageSource, ShaderKind, and the ShaderPayload container) lives in
3//! concinnity-asset; this file keeps the `Component` impl and the
4//! `StageSourceExt::current_platform_source` extension the engine init and
5//! hot-reload paths use. The JSON-args source selection and validation live in
6//! concinnity-world (`source_args`, `check::shader`).
7
8use alloc::string::String;
9
10pub use concinnity_asset::{Shader, ShaderKind, ShaderPayload, StageSource};
11
12use crate::ecs::{Component, PayloadLocator};
13
14/// Resolve the source filename for the current build platform from a stage's
15/// declared `source` / `sources`. Mirrors the build-time selection
16/// (concinnity-world `source_args`) so the hot-reload subsystem picks the
17/// same per-platform source the build read at compile time. Returns `None` when
18/// no current-platform source is declared (e.g. a stage that only declares `glsl`
19/// running on the Metal backend, which loads the embedded GLSL fallback at init
20/// and has no on-disk file to hot-reload). Exposed as an extension trait because
21/// the schema type lives in concinnity-asset.
22pub trait StageSourceExt {
23    /// The source path declared for the running platform, or `None` when the
24    /// stage declares none.
25    fn current_platform_source(&self) -> Option<String>;
26}
27
28impl StageSourceExt for StageSource {
29    fn current_platform_source(&self) -> Option<String> {
30        let platform = crate::platform::Platform::current();
31        if let Some(sources) = &self.sources
32            && let Some(src) = sources.get(platform.key())
33        {
34            return Some(src.clone());
35        }
36        if self.source.is_empty() {
37            return None;
38        }
39        let ext = super::path_extension(&self.source).unwrap_or("");
40        if platform.accepts_ext(ext) {
41            Some(self.source.clone())
42        } else {
43            None
44        }
45    }
46}
47
48impl Component for Shader {
49    const NAME: &'static str = "Shader";
50
51    fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
52        Ok(crate::blob::decode_exact(bytes)?)
53    }
54
55    fn inject_locator(&mut self, locator: PayloadLocator) {
56        self.locator = Some(locator);
57    }
58
59    fn inject_name(&mut self, id: crate::ecs::asset_id::AssetId) {
60        self.asset_id = id;
61    }
62}
63
64/// Returns the platform key used to look up entries in the `sources` map.
65pub fn platform_key() -> &'static str {
66    crate::platform::Platform::current().key()
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use alloc::string::ToString;
73
74    #[test]
75    fn compile_kind_maps_each_stage() {
76        assert_eq!(ShaderKind::Vertex.compile_kind(), "vertex");
77        assert_eq!(ShaderKind::VertexInstanced.compile_kind(), "vertex");
78        assert_eq!(ShaderKind::Fragment.compile_kind(), "fragment");
79        assert_eq!(ShaderKind::default(), ShaderKind::Vertex);
80    }
81
82    #[test]
83    fn current_platform_source_resolves_for_any_backend() {
84        // Declaring every platform source resolves on whichever backend the
85        // test build targets.
86        let stage = StageSource {
87            sources: Some(
88                [
89                    ("metal".to_string(), "v.metal".to_string()),
90                    ("hlsl".to_string(), "v.hlsl".to_string()),
91                    ("glsl".to_string(), "v.glsl".to_string()),
92                ]
93                .into_iter()
94                .collect(),
95            ),
96            ..Default::default()
97        };
98        assert!(stage.current_platform_source().is_some());
99    }
100
101    #[test]
102    fn single_source_resolves_only_for_matching_extensions() {
103        let stage = StageSource {
104            source: "v.metal".to_string(),
105            sources: None,
106        };
107        let platform = crate::platform::Platform::current();
108        assert_eq!(
109            stage.current_platform_source().is_some(),
110            platform.accepts_ext("metal")
111        );
112    }
113}