1use serde::Serialize;
4use serde_json::Value;
5
6use crate::json::JsonError;
7use crate::world::{Component, Entity};
8use iridia_interface::{Interface, ShaderFormat};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Shader {
17 pub format: ShaderFormat,
18 pub bytecode: Vec<u8>,
19 pub interface: Interface,
20}
21
22#[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#[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 (
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}