Skip to main content

concinnity_asset/
shader.rs

1// Shader schema: one asset = one complete shader program (all stages).
2//
3// CRITICAL: `packed_float3` in light structs. In MSL constant buffers `float3`
4// has size=16, but Rust `[f32; 3]` (what the engine sends) has size=12. If you
5// declare `DirectionalLightData` or `PointLightData` with plain `float3`, the
6// color field will read as zeros (black light) and `num_directional` will read
7// garbage, causing ambient-only rendering. Always use `packed_float3` for vector
8// fields in these structs.
9
10use crate::{AssetId, PayloadLocator};
11use alloc::collections::BTreeMap;
12use alloc::string::String;
13use alloc::vec::Vec;
14
15/// A stage slot within a [Shader](#shader).
16///
17/// `VertexInstanced` is the GPU-instanced sibling of `Vertex`, reading per-
18/// instance model matrices instead of a per-draw transform. Required for any
19/// world containing [InstancedProp](#instancedprop) components; otherwise
20/// unused.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "lowercase")]
23#[derive(Default)]
24pub enum ShaderKind {
25    /// Vertex stage for a per-draw transform.
26    #[default]
27    Vertex,
28    /// Fragment stage.
29    Fragment,
30    /// Vertex stage reading per-instance model matrices.
31    #[serde(rename = "vertex_instanced", alias = "vertexinstanced")]
32    VertexInstanced,
33}
34
35impl ShaderKind {
36    /// The compile kind string expected by ShaderCompileArgs.
37    pub fn compile_kind(&self) -> &'static str {
38        match self {
39            ShaderKind::Vertex | ShaderKind::VertexInstanced => "vertex",
40            ShaderKind::Fragment => "fragment",
41        }
42    }
43}
44
45/// Source declaration for one stage of a [Shader](#shader).
46///
47/// Provide either `source` (single platform) or `sources` (multi-platform).
48/// When both are present, `sources` takes priority for the current platform.
49///
50/// **Platform keys:** `"metal"` (macOS), `"hlsl"` (Windows), `"glsl"` (Linux/Vulkan).
51#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
52pub struct StageSource {
53    /// Single-platform source path; used when `sources` is absent or lacks the current platform key.
54    #[serde(default)]
55    pub source: String,
56    /// Per-platform source paths keyed by `"metal"`, `"hlsl"`, or `"glsl"`. Takes priority over `source`.
57    #[serde(default)]
58    pub sources: Option<BTreeMap<String, String>>,
59}
60
61impl StageSource {}
62
63/// Declares a custom shader program: the vertex and fragment stages, plus the
64/// optional GPU-instanced vertex stage.
65///
66/// **A Shader is entirely optional.** The engine ships its own main-pass
67/// program and uses it for every draw a Shader does not claim, so a world that
68/// wants standard lighting declares no Shader at all. Declare one only to
69/// replace that program with your own. The shadow pass is engine-internal (no
70/// Shader stage of its own); enable or size it with `shadow_map_size` in
71/// [GraphicsConfig](#graphicsconfig).
72///
73/// The engine-internal shadow map covers a ±20 m world-space region centred at
74/// the origin with 80 m depth. For larger scenes, increase `shadow_map_size` in
75/// [GraphicsConfig](#graphicsconfig) to maintain resolution.
76///
77/// A stage that resolves no source for the running backend falls back to the
78/// engine's own program for that stage, so a Shader may cover only the
79/// platforms it has sources for.
80///
81/// # More than one Shader
82///
83/// The first declared Shader is the world's default: everything renders with it
84/// unless a [Material](#material) names another one through its `shader` field.
85/// A world may declare up to 8 Shaders in total.
86///
87/// Three rules come with the second Shader, all enforced at build time:
88///
89/// - **Every fragment stage must define `fragment_main_bindless`.** Multi-Shader
90///   worlds render through the GPU-driven bindless path, which is the only path
91///   that can switch programs per draw. A single-Shader world has no such
92///   requirement and may define just `fragment_main`. This applies to `.metal`
93///   sources, which carry one program per entry point; an `.hlsl` or GLSL stage
94///   compiles a single `main`, so there is no entry point to pick -- what it must
95///   match instead is the bindless binding layout (see below).
96/// - **Instanced, skinned, and voxel-chunk draws always use the world default.**
97///   A Material naming a Shader cannot be used by an
98///   [InstancedProp](#instancedprop), a [SkinnedMesh](#skinnedmesh), or a
99///   [VoxelWorld](#voxelworld); give those a Material without one.
100/// - **At most 8 Shaders**, the world default included.
101///
102/// Planar reflections are the one case with no build-time signal: a surface
103/// reflected in a mirror is drawn with the world default Shader regardless of
104/// its Material. Reflection probe cubes capture it the same way.
105///
106/// A non-default Shader's stages must be written against the engine's **bindless**
107/// binding layout, not the per-draw one: the material, transform, and texture
108/// indices come from the per-frame object buffer rather than per-draw constants.
109///
110/// A Shader referenced only by materials belonging to one [Scene](#scene) is
111/// owned by that scene: its pipeline is built when the scene loads (behind the
112/// loading screen, alongside that scene's textures and meshes) and released when
113/// the scene unloads. A Shader used across scenes, or by the world default,
114/// loads at startup.
115///
116/// **Custom shader vertex layout**: the engine always supplies vertices with 5
117/// attributes at a fixed 56-byte stride. Any custom `.metal` shader **must** declare
118/// `struct Vertex` exactly as shown below: wrong attribute indices cause tangent
119/// data to be read as vertex colour, producing red/green/blue geometry:
120///
121/// ```metal
122/// struct Vertex {
123///     float3 pos     [[attribute(0)]];  // offset  0
124///     float3 normal  [[attribute(1)]];  // offset 12
125///     float3 tangent [[attribute(2)]];  // offset 24
126///     float3 color   [[attribute(3)]];  // offset 36
127///     float2 uv      [[attribute(4)]];  // offset 48
128/// };
129/// ```
130///
131/// Buffer and texture bindings that must match:
132///
133/// ```metal
134/// struct DirectionalLightData {
135///     packed_float3 direction;
136///     float         intensity;
137///     packed_float3 color;
138///     float         _pad;
139/// };
140///
141/// struct PointLightData {
142///     packed_float3 position;
143///     float         range;
144///     packed_float3 color;
145///     float         intensity;
146/// };
147///
148/// struct ShadowUniforms {
149///     float4x4 light_vp;
150/// };
151/// ```
152#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
153pub struct Shader {
154    /// Asset identity; injected via `inject_name`. Not part of `args`.
155    #[serde(skip)]
156    pub asset_id: AssetId,
157    /// The vertex stage. Required.
158    pub vertex: StageSource,
159    /// The fragment stage. Required.
160    pub fragment: StageSource,
161    /// The GPU-instanced vertex stage. Required only for worlds with
162    /// [InstancedProp](#instancedprop) components.
163    #[serde(default)]
164    pub vertex_instanced: Option<StageSource>,
165    /// Injected at load time from BlobAssetDef::payload.
166    #[serde(skip)]
167    pub locator: Option<PayloadLocator>,
168}
169
170impl Shader {
171    /// The declared source for `kind`, if that stage is present.
172    pub fn stage(&self, kind: ShaderKind) -> Option<&StageSource> {
173        match kind {
174            ShaderKind::Vertex => Some(&self.vertex),
175            ShaderKind::Fragment => Some(&self.fragment),
176            ShaderKind::VertexInstanced => self.vertex_instanced.as_ref(),
177        }
178    }
179}
180
181/// The compiled payload a [`Shader`] carries in the blob: every compiled stage,
182/// tagged by kind. Written by the cook, decoded once by the renderer at load.
183#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
184pub struct ShaderPayload {
185    /// The compiled bytes of each stage, tagged by kind.
186    pub stages: Vec<(ShaderKind, Vec<u8>)>,
187}
188
189impl ShaderPayload {
190    /// Serialize the payload for the blob.
191    pub fn encode(&self) -> Result<Vec<u8>, postcard::Error> {
192        postcard::to_allocvec(self)
193    }
194
195    /// Read a payload back out of the blob.
196    pub fn decode(bytes: &[u8]) -> Result<Self, postcard::Error> {
197        postcard::from_bytes(bytes)
198    }
199
200    /// The compiled bytes for `kind`, if that stage was compiled.
201    pub fn stage(&self, kind: ShaderKind) -> Option<&[u8]> {
202        self.stages
203            .iter()
204            .find(|(k, _)| *k == kind)
205            .map(|(_, b)| b.as_slice())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use alloc::vec;
213
214    #[test]
215    fn payload_round_trips_and_indexes_by_kind() {
216        let payload = ShaderPayload {
217            stages: vec![
218                (ShaderKind::Vertex, vec![1, 2, 3]),
219                (ShaderKind::Fragment, vec![4, 5]),
220            ],
221        };
222        let bytes = payload.encode().expect("encode");
223        let decoded = ShaderPayload::decode(&bytes).expect("decode");
224        assert_eq!(decoded, payload);
225        assert_eq!(decoded.stage(ShaderKind::Vertex), Some(&[1u8, 2, 3][..]));
226        assert_eq!(decoded.stage(ShaderKind::Fragment), Some(&[4u8, 5][..]));
227        assert_eq!(decoded.stage(ShaderKind::VertexInstanced), None);
228    }
229
230    #[test]
231    fn stage_lookup_covers_every_kind() {
232        let s = Shader::default();
233        assert!(s.stage(ShaderKind::Vertex).is_some());
234        assert!(s.stage(ShaderKind::Fragment).is_some());
235        assert!(s.stage(ShaderKind::VertexInstanced).is_none());
236    }
237
238    #[test]
239    fn the_instanced_vertex_stage_compiles_as_a_vertex_stage() {
240        assert_eq!(ShaderKind::Vertex.compile_kind(), "vertex");
241        assert_eq!(ShaderKind::VertexInstanced.compile_kind(), "vertex");
242        assert_eq!(ShaderKind::Fragment.compile_kind(), "fragment");
243        assert_eq!(ShaderKind::default(), ShaderKind::Vertex);
244    }
245
246    #[test]
247    fn stage_kinds_parse_from_their_authored_spellings() {
248        let kind = |s: &str| serde_json::from_str::<ShaderKind>(s).unwrap();
249        assert_eq!(kind(r#""vertex""#), ShaderKind::Vertex);
250        assert_eq!(kind(r#""fragment""#), ShaderKind::Fragment);
251        assert_eq!(kind(r#""vertex_instanced""#), ShaderKind::VertexInstanced);
252        // The unseparated spelling is accepted as an alias.
253        assert_eq!(kind(r#""vertexinstanced""#), ShaderKind::VertexInstanced);
254        assert_eq!(
255            serde_json::to_string(&ShaderKind::VertexInstanced).unwrap(),
256            r#""vertex_instanced""#
257        );
258    }
259
260    #[test]
261    fn a_shader_parses_from_authored_args() {
262        let s: Shader = serde_json::from_str(
263            r#"{"vertex":{"sources":{"metal":"my.metal"}},"fragment":{"source":"my.metal"}}"#,
264        )
265        .unwrap();
266        assert_eq!(s.fragment.source, "my.metal");
267        assert_eq!(
268            s.vertex.sources.as_ref().expect("per-platform")["metal"],
269            "my.metal"
270        );
271        // A world with no instanced props declares no instanced vertex stage.
272        assert!(s.vertex_instanced.is_none());
273        // The identity and payload locator are injected, never authored.
274        assert_eq!(s.asset_id, AssetId::default());
275        assert!(s.locator.is_none());
276
277        let bytes = postcard::to_allocvec(&s).unwrap();
278        let back: Shader = postcard::from_bytes(&bytes).unwrap();
279        assert_eq!(back.fragment.source, "my.metal");
280    }
281
282    #[test]
283    fn an_empty_payload_has_no_stages() {
284        let payload = ShaderPayload::default();
285        assert!(payload.stages.is_empty());
286        assert_eq!(payload.stage(ShaderKind::Vertex), None);
287        assert_eq!(
288            ShaderPayload::decode(&payload.encode().unwrap()),
289            Ok(payload)
290        );
291    }
292
293    #[test]
294    fn decoding_garbage_is_an_error_not_a_panic() {
295        assert!(ShaderPayload::decode(&[0xff, 0xff, 0xff]).is_err());
296    }
297}