1use crate::shader::{LiftedShader, ShaderBinding, StageBinding};
3use crate::{Component, Entity, Geometry, World};
4use iridia_interface::ResourceId;
5use std::collections::{BTreeMap, BTreeSet};
6use std::sync::Arc;
7#[derive(Clone, Debug, Default)]
11pub struct ShaderSpecialisations(pub Vec<StageSpecialisation>);
12#[derive(Clone, Debug)]
14pub struct StageSpecialisation {
15 pub shader: Entity,
16 pub program: Option<Arc<iridia::Program>>,
17 pub words: BTreeMap<(ResourceId, u32), u32>,
18 pub removed: Vec<ResourceId>,
19 pub folded: usize,
20 pub refusal: Option<String>,
21}
22impl ShaderSpecialisations {
23 pub fn of(world: &World, binding: &ShaderBinding) -> Self {
26 Self::shared(world, binding, &mut SpecialisationCache::default())
27 }
28 pub fn program(&self, shader: Entity) -> Option<&Arc<iridia::Program>> {
30 self.0
31 .iter()
32 .find(|s| s.shader == shader)
33 .and_then(|s| s.program.as_ref())
34 }
35 pub fn shared(world: &World, binding: &ShaderBinding, cache: &mut SpecialisationCache) -> Self {
38 Self(
39 binding
40 .stages
41 .iter()
42 .map(|stage| Self::stage(world, stage, cache))
43 .collect(),
44 )
45 }
46 fn stage(world: &World, stage: &StageBinding, cache: &mut SpecialisationCache) -> StageSpecialisation {
47 let refused = |why: &str| StageSpecialisation {
48 shader: stage.shader,
49 program: None,
50 words: Default::default(),
51 removed: vec![],
52 folded: 0,
53 refusal: Some(why.to_owned()),
54 };
55 let Some(source) = world.get::<LiftedShader>(stage.shader) else {
56 return refused("source has not supplied an exact lift");
57 };
58 let known = stage.known_words();
59 let keys: BTreeSet<_> = known.keys().copied().collect();
61 let selected = match cache.selected.entry((stage.shader, keys)).or_insert_with(|| {
62 iridia::specialise::feature_words(&source.0, &known).map(|w| w.into_keys().collect())
63 }) {
64 Ok(selected) => selected,
65 Err(why) => return refused(why),
66 };
67 let words: Words = known
68 .into_iter()
69 .filter(|(key, _)| selected.contains(key))
70 .collect();
71 match cache
72 .programs
73 .entry((stage.shader, words))
74 .or_insert_with_key(|(_, words)| {
75 iridia::specialise::specialise(&source.0, words).map(|result| Specialised {
76 program: Arc::new(result.program),
77 words: result.words,
78 removed: result.unread,
79 folded: result.folded,
80 })
81 }) {
82 Ok(shared) => StageSpecialisation {
83 shader: stage.shader,
84 program: Some(shared.program.clone()),
85 words: shared.words.clone(),
86 removed: shared.removed.clone(),
87 folded: shared.folded,
88 refusal: None,
89 },
90 Err(why) => refused(why),
91 }
92 }
93}
94
95type Word = (ResourceId, u32);
97type Words = BTreeMap<Word, u32>;
98
99#[derive(Debug)]
101struct Specialised {
102 program: Arc<iridia::Program>,
103 words: Words,
104 removed: Vec<ResourceId>,
105 folded: usize,
106}
107
108#[derive(Debug, Default)]
111pub struct SpecialisationCache {
112 selected: BTreeMap<(Entity, BTreeSet<Word>), Result<BTreeSet<Word>, String>>,
114 programs: BTreeMap<(Entity, Words), Result<Specialised, String>>,
116}
117impl SpecialisationCache {
118 pub fn programs(&self) -> usize {
120 self.programs.values().filter(|p| p.is_ok()).count()
121 }
122}
123impl Component for ShaderSpecialisations {
124 const NAME: &'static str = "core.shader_specialisations";
125 const GEOMETRY: Geometry = Geometry::None;
126 fn references(&self, out: &mut Vec<Entity>) {
127 out.extend(self.0.iter().map(|s| s.shader));
128 }
129 fn to_json(&self) -> Result<Option<serde_json::Value>, crate::JsonError> {
130 crate::json::to_value(&self.0.iter().map(|s|serde_json::json!({
131 "shader":s.shader,"words":s.words.iter().map(|((resource,offset),word)|(*resource,*offset,*word)).collect::<Vec<_>>(),
132 "resources_removed":s.removed,"expressions_folded":s.folded,"refusal":s.refusal,
133 })).collect::<Vec<_>>()).map(Some)
134 }
135 fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
136 let binding = world
137 .get::<ShaderBinding>(entity)
138 .ok_or("specialisations have no source binding")?;
139 for (index, stage) in self.0.iter().enumerate() {
140 let source = binding
141 .stages
142 .iter()
143 .find(|s| s.shader == stage.shader)
144 .ok_or("specialisation names an unbound shader")?;
145 if self.0.iter().take(index).any(|s| s.shader == stage.shader) {
146 return Err("duplicate stage specialisation".into());
147 }
148 let known = source.known_words();
149 if stage.words.iter().any(|(k, v)| known.get(k) != Some(v)) {
150 return Err("specialisation freezes a word the binding does not state".into());
151 }
152 if let Some(program) = &stage.program {
153 program.validate().map_err(|e| format!("{e:?}"))?;
154 }
155 }
156 Ok(())
157 }
158 fn apply_linear(&self, _: &crate::Linear) -> crate::world::Linearity<Self> {
159 crate::world::Linearity::Invariant
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::shader::{ConstantBinding, ConstantRole};
167 use iridia::{ResourceKind, Stage};
168
169 fn program() -> iridia::Program {
172 iridia_mojo::lift(
173 "uniform vec4 ps_uniforms_vec4[1];\nuniform sampler2D ps_s0;\nvarying vec4 vTexCoord0;\nvec4 ps_r0;\nvoid main() {\nps_r0 = texture2D(ps_s0,vTexCoord0.xy);\nif (ps_uniforms_vec4[0].x != 0.0) {\nps_r0 = ps_r0 * ps_uniforms_vec4[0].yyyy;\n} else {\nps_r0 = vTexCoord0;\n}\ngl_FragColor = ps_r0;\n}",
174 Stage::Pixel,
175 &iridia_mojo::Hints {
176 target: "fixture".into(),
177 constants: vec![iridia_mojo::Constant {
178 name: "SwitchAndScale".into(),
179 offset: 0,
180 bytes: 16,
181 width: 4,
182 }],
183 samplers: vec![iridia_mojo::Sampler {
184 glsl_name: "ps_s0".into(),
185 name: "image".into(),
186 slot: 0,
187 }],
188 attributes: vec![],
189 },
190 )
191 .unwrap()
192 }
193
194 fn binding(shader: Entity, buffer: ResourceId, switch: f32, scale: f32) -> ShaderBinding {
196 ShaderBinding {
197 stages: vec![StageBinding {
198 constants: vec![ConstantBinding {
199 resource: buffer,
200 offset: 0,
201 size: 8,
202 name: None,
203 role: ConstantRole::Material(vec![switch.to_bits(), scale.to_bits()]),
204 lines: None,
205 }],
206 ..StageBinding::new(Stage::Pixel, shader)
207 }],
208 state: Default::default(),
209 }
210 }
211
212 #[test]
215 fn materials_share_a_specialisation_only_when_the_selected_words_are_equal() {
216 let source = program();
217 let buffer = source
218 .interface
219 .resources
220 .iter()
221 .find(|r| matches!(r.kind, ResourceKind::ConstantBuffer { .. }))
222 .unwrap()
223 .id;
224 let mut world = World::new();
225 let shader = world.spawn();
226 world.insert(shader, LiftedShader(Arc::new(source)));
227 let mut cache = SpecialisationCache::default();
228 let mut of = |switch: f32, scale: f32| {
229 let set =
230 ShaderSpecialisations::shared(&world, &binding(shader, buffer, switch, scale), &mut cache);
231 let [stage] = &set.0[..] else { panic!("one stage") };
232 assert_eq!(stage.refusal, None);
233 (stage.program.clone().unwrap(), stage.words.clone())
234 };
235 let (on, on_words) = of(1.0, 2.0);
236 let (on_again, _) = of(1.0, 3.0);
237 let (off, off_words) = of(0.0, 2.0);
238 assert_eq!(on_words, Words::from([((buffer, 0), 1f32.to_bits())]));
240 assert_eq!(off_words, Words::from([((buffer, 0), 0f32.to_bits())]));
241 assert!(Arc::ptr_eq(&on, &on_again));
242 assert!(!Arc::ptr_eq(&on, &off));
243 let textures = |p: &iridia::Program| {
245 p.interface
246 .resources
247 .iter()
248 .filter(|r| matches!(r.kind, ResourceKind::Texture { .. }))
249 .count()
250 };
251 assert_eq!((textures(&on), textures(&off)), (1, 0));
252 assert_eq!(cache.programs(), 2);
253 }
254
255 #[test]
257 fn a_stage_without_an_exact_lift_is_refused() {
258 let mut world = World::new();
259 let shader = world.spawn();
260 let set = ShaderSpecialisations::of(&world, &binding(shader, ResourceId(0), 1.0, 1.0));
261 assert_eq!(
262 set.0[0].refusal.as_deref(),
263 Some("source has not supplied an exact lift")
264 );
265 assert!(set.0[0].program.is_none());
266 }
267}