Skip to main content

primase_core/shader/
program.rs

1//! `core.shader`: one shader program as stored, with its interface.
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::json::JsonError;
7use crate::world::{Component, Entity};
8use iridia_interface::{Interface, ShaderFormat};
9
10/// `core.shader`: one shader program, on its own entity. The bytecode (or source text) is kept exactly as the game
11/// stored it, in its [`ShaderFormat`]; what the program exposes, its stage, varyings and resources, is in primase's
12/// vocabulary ([`Interface`]). Its operations may be supplied separately as [`LiftedShader`] when lifting
13/// needs container metadata; otherwise consumers lift these bytes on demand. A material's
14/// `core.shader_binding` refers to the entity.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Shader {
17    pub format: ShaderFormat,
18    pub bytecode: Vec<u8>,
19    pub interface: Interface,
20}
21
22/// An exact lift of the accompanying [`Shader`], supplied by its reader or resolver when lifting
23/// needs container metadata absent from the shader bytes. Any format may use this transport.
24/// This is neither a replacement shader nor a material specialization: no constants or branches
25/// may be frozen here. The original bytes and interface remain on `Shader`; derivations belong
26/// to [`super::ShaderSpecialisations`] or a consumer's explicit derivation mechanism.
27/// Consumers may use this instead of re-lifting.
28#[derive(Clone, Debug)]
29pub struct LiftedShader(pub std::sync::Arc<iridia::Program>);
30
31impl Component for LiftedShader {
32    const NAME: &'static str = "core.lifted_shader";
33    const GEOMETRY: crate::Geometry = crate::Geometry::None;
34    fn verify(&self, e: Entity, w: &crate::World) -> Result<(), String> {
35        let shader = w.get::<Shader>(e).ok_or("lifted shader has no original shader")?;
36        if self.0.interface != shader.interface || self.0.stage != shader.interface.stage {
37            return Err("lifted shader differs from the original shader interface".into());
38        }
39        self.0.validate().map(|_| ()).map_err(|e| format!("{e:?}"))
40    }
41    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
42        crate::world::Linearity::Invariant
43    }
44}
45
46/// What [`Shader::to_json`] shows: the bytecode by its size (it is bulk data), the rest in full.
47#[derive(Serialize)]
48struct View<'a> {
49    format: ShaderFormat,
50    bytecode_bytes: usize,
51    interface: &'a Interface,
52}
53
54impl Component for Shader {
55    const NAME: &'static str = "core.shader";
56    const GEOMETRY: crate::Geometry = crate::Geometry::None;
57    fn to_json(&self) -> Result<Option<Value>, JsonError> {
58        crate::json::to_value(&View {
59            format: self.format,
60            bytecode_bytes: self.bytecode.len(),
61            interface: &self.interface,
62        })
63        .map(Some)
64    }
65    fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
66        if self.bytecode.is_empty() {
67            return Err("a shader without bytecode".into());
68        }
69        self.interface.verify()
70    }
71    fn payload_bytes(&self) -> u64 {
72        self.bytecode.len() as u64
73    }
74    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
75        crate::world::Linearity::Invariant
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::World;
83    use iridia_interface::{
84        BufferKind, BufferLayout, Constant, ConstantType, Label, Resource, ResourceId, ResourceKind,
85        ResourceProvenance, ScalarType, Stage, StageFacts, Value as Lanes, Varying, VaryingProvenance,
86    };
87
88    fn varying(name: &str, index: u32, width: u8) -> Varying {
89        Varying {
90            label: Label {
91                name: name.into(),
92                index,
93            },
94            system: None,
95            value: Lanes {
96                scalar: ScalarType::Float,
97                width,
98            },
99            interp: None,
100            used: true,
101            provenance: VaryingProvenance::default(),
102        }
103    }
104    fn buffer(id: u32, size: u32, offset: u32) -> Resource {
105        Resource {
106            id: ResourceId(id),
107            kind: ResourceKind::ConstantBuffer {
108                layout: BufferLayout {
109                    size,
110                    constants: vec![Constant {
111                        name: "tint".into(),
112                        offset,
113                        ty: ConstantType::Vector(Lanes {
114                            scalar: ScalarType::Float,
115                            width: 4,
116                        }),
117                        default: None,
118                    }],
119                },
120            },
121            count: 1,
122            provenance: ResourceProvenance::default(),
123        }
124    }
125    fn shader(interface: Interface) -> Shader {
126        Shader {
127            format: ShaderFormat::Dxbc,
128            bytecode: vec![1, 2, 3, 4],
129            interface,
130        }
131    }
132    fn interface() -> Interface {
133        Interface {
134            stage: Stage::Pixel,
135            inputs: vec![varying("TEXCOORD", 0, 2)],
136            outputs: vec![varying("SV_Target", 0, 4)],
137            resources: vec![buffer(0, 16, 0)],
138            facts: iridia_interface::StageFacts::None,
139        }
140    }
141
142    #[test]
143    fn a_shader_shows_its_bytecode_by_size_and_its_interface_in_full() {
144        let mut w = World::new();
145        let e = w.spawn();
146        w.insert(e, shader(interface()));
147        let json = w.get::<Shader>(e).unwrap().to_json().unwrap().unwrap();
148        assert_eq!(json["format"], "dxbc");
149        assert_eq!(json["bytecode_bytes"], 4);
150        assert_eq!(json["interface"]["inputs"][0]["label"]["name"], "TEXCOORD");
151        assert_eq!(w.get::<Shader>(e).unwrap().payload_bytes(), 4);
152    }
153
154    #[test]
155    fn a_consistent_interface_verifies_and_each_broken_rule_is_named() {
156        let mut w = World::new();
157        let e = w.spawn();
158        assert_eq!(shader(interface()).verify(e, &w), Ok(()));
159        type Break = Box<dyn Fn(&mut Interface)>;
160        let broken: Vec<(Break, &str)> = vec![
161            (Box::new(|i| i.inputs.push(varying("TEXCOORD", 0, 4))), "twice"),
162            (Box::new(|i| i.inputs[0].value.width = 5), "width"),
163            (Box::new(|i| i.resources.push(buffer(0, 16, 0))), "id 0"),
164            (Box::new(|i| i.resources[0].count = 0), "no bindings"),
165            // facts of another stage, or a hull program without its patch
166            (
167                Box::new(|i| i.facts = StageFacts::VertexWithoutPosition),
168                "Pixel program with",
169            ),
170            (Box::new(|i| i.stage = Stage::Hull), "without its patch facts"),
171            (Box::new(|i| i.resources = vec![buffer(0, 16, 4)]), "past the end"),
172            (
173                Box::new(|i| {
174                    i.resources[0].kind = ResourceKind::Buffer(BufferKind::Structured { stride: 0 });
175                }),
176                "stride",
177            ),
178        ];
179        for (break_it, expect) in broken {
180            let mut i = interface();
181            break_it(&mut i);
182            let message = shader(i).verify(e, &w).unwrap_err();
183            assert!(message.contains(expect), "{expect}: {message}");
184        }
185        let mut empty = shader(interface());
186        empty.bytecode.clear();
187        assert!(empty.verify(e, &w).is_err());
188    }
189}
190
191#[cfg(test)]
192mod lifted_tests {
193    use super::*;
194    #[test]
195    fn an_exact_lift_requires_its_original_interface() {
196        let program = iridia::Program {
197            module: naga::front::wgsl::parse_str("@fragment fn main() {}").unwrap(),
198            stage: iridia::Stage::Pixel,
199            entry: "main".into(),
200            interface: Interface {
201                stage: iridia::Stage::Pixel,
202                inputs: vec![],
203                outputs: vec![],
204                resources: vec![],
205                facts: iridia_interface::StageFacts::None,
206            },
207            bindings: vec![],
208            conventions: iridia::Conventions::D3D,
209            modes: Default::default(),
210            notes: vec![],
211            provenance: iridia::Provenance {
212                format: ShaderFormat::Glsl,
213                target: "fixture".into(),
214                lifter: "fixture".into(),
215                lifter_version: 1,
216                source_hash: None,
217                instruction_count: None,
218            },
219        };
220        let mut world = crate::World::new();
221        let e = world.spawn();
222        let lifted = LiftedShader(std::sync::Arc::new(program));
223        assert!(lifted.verify(e, &world).is_err());
224        world.insert(
225            e,
226            Shader {
227                format: ShaderFormat::Glsl,
228                bytecode: b"void main() {}".to_vec(),
229                interface: lifted.0.interface.clone(),
230            },
231        );
232        assert!(lifted.verify(e, &world).is_ok());
233        world.get_mut::<Shader>(e).unwrap().interface.stage = iridia::Stage::Vertex;
234        assert!(lifted.verify(e, &world).is_err());
235    }
236}