1use iridia::flow::{Node, Values};
5use iridia_interface::{Interface, ScalarType, Stage};
6use serde::Serialize;
7
8use super::{Attribute, InputRole, StageBinding};
9use crate::pbr::TextureSlot;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
15pub struct FallbackCoordinates {
16 pub slot: TextureSlot,
17 pub absent_texture_set: u8,
18 pub absent_stream: [u32; 2],
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
24pub struct FallbackCoordinateCopies {
25 pub coordinates: FallbackCoordinates,
26 pub lanes: [Option<u8>; 4],
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum InputFallbackValue {
33 Normal,
35 MaterialVertexColour { absent: [u32; 4] },
38 MaterialCoordinates(FallbackCoordinates),
40 Literal([u32; 4]),
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
48pub struct InputFallback {
49 pub value: InputFallbackValue,
50 pub coordinate_copies: Option<FallbackCoordinateCopies>,
51}
52
53impl InputFallback {
54 pub fn new(value: InputFallbackValue) -> Self {
56 Self {
57 value,
58 coordinate_copies: None,
59 }
60 }
61
62 pub fn check(&self) -> Result<(), String> {
63 let finite = |words: &[u32]| {
64 if words.iter().all(|word| f32::from_bits(*word).is_finite()) {
65 Ok(())
66 } else {
67 Err("replacement input contains a non-finite float word".to_owned())
68 }
69 };
70 match &self.value {
71 InputFallbackValue::Normal => {}
72 InputFallbackValue::MaterialVertexColour { absent } | InputFallbackValue::Literal(absent) => {
73 finite(absent)?
74 }
75 InputFallbackValue::MaterialCoordinates(coordinates) => finite(&coordinates.absent_stream)?,
76 }
77 if let Some(copies) = &self.coordinate_copies {
78 finite(&copies.coordinates.absent_stream)?;
79 if copies.lanes.iter().flatten().any(|lane| *lane > 1) {
80 return Err("replacement coordinate-copy lane is neither u nor v".to_owned());
81 }
82 }
83 Ok(())
84 }
85}
86
87fn base_coordinates() -> FallbackCoordinates {
88 FallbackCoordinates {
89 slot: TextureSlot::BaseColor,
90 absent_texture_set: 0,
91 absent_stream: [0; 2],
92 }
93}
94
95pub fn bind_surface_input_fallbacks(stage: &mut StageBinding, interface: &Interface) -> usize {
100 if stage.stage != Stage::Pixel || interface.stage != Stage::Pixel {
101 return 0;
102 }
103 let mut count = 0;
104 for input in &mut stage.inputs {
105 if !matches!(input.role, InputRole::Previous | InputRole::Unknown { .. }) {
106 continue;
107 }
108 let Some(varying) = interface.inputs.iter().find(|v| v.label == input.label) else {
109 continue;
110 };
111 if varying.system.is_some() || varying.value.scalar != ScalarType::Float || input.label.index != 0 {
112 continue;
113 }
114 let (value, why) = match input.label.name.to_ascii_uppercase().as_str() {
115 "NORMAL" => (InputFallbackValue::Normal, "the mesh normal"),
116 "COLOR" => (
117 InputFallbackValue::MaterialVertexColour {
118 absent: [1.0f32.to_bits(); 4],
119 },
120 "the mesh vertex colour",
121 ),
122 "TEXCOORD" => (
123 InputFallbackValue::MaterialCoordinates(base_coordinates()),
124 "the mesh UV, in both pairs",
125 ),
126 "TANGENT" => (
127 InputFallbackValue::Literal([1.0f32.to_bits(), 0, 0, 1.0f32.to_bits()]),
128 "stand-in: the +X tangent (the scene has none)",
129 ),
130 _ => continue,
131 };
132 input.role = InputRole::PreviousOr {
133 fallback: InputFallback::new(value),
134 why: why.into(),
135 };
136 count += 1;
137 }
138 count
139}
140
141pub fn bind_fallback_uv_copies(
146 pixel: &mut StageBinding,
147 program: &iridia::Program,
148 vertex: &StageBinding,
149 coordinates: FallbackCoordinates,
150) -> usize {
151 if pixel.stage != Stage::Pixel || vertex.stage != Stage::Vertex || program.stage != Stage::Vertex {
152 return 0;
153 }
154 let Ok(values) = Values::of_program(program) else {
155 return 0;
156 };
157 let mut copies = std::collections::BTreeMap::new();
158 for (&(output, lane), &id) in values.outputs() {
159 let out = &program.interface.outputs[output];
160 let Node::Input { input, lane: from } = values.node(id) else {
161 continue;
162 };
163 let source = &program.interface.inputs[*input];
164 let is_uv = vertex
165 .inputs
166 .iter()
167 .any(|role| role.label == source.label && role.role == InputRole::Stream(Attribute::TexCoord(0)));
168 if out.system.is_none()
169 && out.value.scalar == ScalarType::Float
170 && source.value.scalar == ScalarType::Float
171 && *from < 2
172 && is_uv
173 {
174 copies.insert(
175 (out.label.name.to_ascii_uppercase(), out.label.index, lane),
176 *from,
177 );
178 }
179 }
180 let mut count = 0;
181 for input in &mut pixel.inputs {
182 let lanes = std::array::from_fn(|lane| {
183 copies
184 .get(&(
185 input.label.name.to_ascii_uppercase(),
186 input.label.index,
187 lane as u8,
188 ))
189 .copied()
190 });
191 if lanes.iter().all(Option::is_none) {
192 continue;
193 }
194 match &mut input.role {
195 InputRole::PreviousOr { fallback, .. } => {
196 fallback.coordinate_copies = Some(FallbackCoordinateCopies {
197 coordinates: coordinates.clone(),
198 lanes,
199 });
200 }
201 InputRole::Previous | InputRole::Unknown { .. } => {
202 input.role = InputRole::PreviousOr {
203 fallback: InputFallback {
204 value: InputFallbackValue::Literal([0; 4]),
205 coordinate_copies: Some(FallbackCoordinateCopies {
206 coordinates: coordinates.clone(),
207 lanes,
208 }),
209 },
210 why: "stand-in: zero (the scene has no such stream)".into(),
211 };
212 }
213 _ => continue,
214 }
215 count += 1;
216 }
217 count
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::shader::InputBinding;
224 use iridia_interface::{Label, Value, Varying, VaryingProvenance};
225
226 fn varying(name: &str, index: u32, scalar: ScalarType, width: u8) -> Varying {
227 Varying {
228 label: Label {
229 name: name.into(),
230 index,
231 },
232 system: None,
233 value: Value { scalar, width },
234 interp: None,
235 used: true,
236 provenance: VaryingProvenance::default(),
237 }
238 }
239
240 #[test]
241 fn standard_surface_fallbacks_are_explicit_and_leave_other_labels_and_roles_alone() {
242 let interface = Interface {
243 stage: Stage::Pixel,
244 inputs: vec![
245 varying("NORMAL", 0, ScalarType::Float, 3),
246 varying("COLOR", 0, ScalarType::Float, 4),
247 varying("TEXCOORD", 0, ScalarType::Float, 4),
248 varying("TANGENT", 0, ScalarType::Float, 4),
249 varying("TEXCOORD", 7, ScalarType::Float, 4),
250 varying("NORMAL", 1, ScalarType::Float, 3),
251 varying("TANGENT", 1, ScalarType::Uint, 4),
252 ],
253 outputs: vec![],
254 resources: vec![],
255 facts: Default::default(),
256 };
257 let mut stage = StageBinding::new(Stage::Pixel, crate::World::new().spawn());
258 stage.inputs = interface
259 .inputs
260 .iter()
261 .map(|v| InputBinding {
262 label: v.label.clone(),
263 role: InputRole::Previous,
264 })
265 .collect();
266 assert_eq!(bind_surface_input_fallbacks(&mut stage, &interface), 4);
267 for input in &stage.inputs[..4] {
268 let InputRole::PreviousOr { fallback, why } = &input.role else {
269 panic!("explicit fallback")
270 };
271 assert!(!why.is_empty());
272 assert!(fallback.check().is_ok());
273 }
274 assert!(
275 stage.inputs[4..]
276 .iter()
277 .all(|input| input.role == InputRole::Previous)
278 );
279 assert_eq!(
280 bind_surface_input_fallbacks(&mut stage, &interface),
281 0,
282 "idempotent"
283 );
284 stage.inputs[0].role = InputRole::Stream(Attribute::Normal);
285 assert_eq!(bind_surface_input_fallbacks(&mut stage, &interface), 0);
286 assert_eq!(stage.inputs[0].role, InputRole::Stream(Attribute::Normal));
287 }
288
289 #[test]
290 fn fallback_words_and_copy_masks_refuse_nonfinite_or_undefined_components() {
291 let mut fallback = InputFallback::new(InputFallbackValue::Literal([0; 4]));
292 assert!(fallback.check().is_ok());
293 fallback.value = InputFallbackValue::Literal([f32::NAN.to_bits(); 4]);
294 assert!(fallback.check().is_err());
295 fallback.value = InputFallbackValue::MaterialVertexColour {
296 absent: [f32::INFINITY.to_bits(); 4],
297 };
298 assert!(fallback.check().is_err());
299 fallback.value = InputFallbackValue::MaterialCoordinates(base_coordinates());
300 fallback.coordinate_copies = Some(FallbackCoordinateCopies {
301 coordinates: base_coordinates(),
302 lanes: [Some(2), None, None, None],
303 });
304 assert!(fallback.check().is_err());
305 fallback
306 .coordinate_copies
307 .as_mut()
308 .expect("owned fallback fixture")
309 .lanes[0] = Some(1);
310 assert!(fallback.check().is_ok());
311 }
312
313 #[test]
314 fn replacement_copy_masks_follow_only_bare_uv_lanes_in_the_vertex_program() {
315 let mut interface = Interface {
316 stage: Stage::Vertex,
317 inputs: vec![varying("packed_uv", 0, ScalarType::Float, 2)],
318 outputs: vec![varying("custom_varying", 2, ScalarType::Float, 4)],
319 resources: vec![],
320 facts: Default::default(),
321 };
322 let mut position = varying("position", 0, ScalarType::Float, 4);
323 position.system = Some(iridia_interface::SystemValue::ClipPosition);
324 interface.outputs.push(position);
325 let program = iridia::Program {
326 module: naga::front::wgsl::parse_str("struct Out { @location(0) @interpolate(perspective) uv: vec4<f32>, @builtin(position) position: vec4<f32> } @vertex fn main(@location(0) uv: vec2<f32>) -> Out { var out: Out; out.uv = vec4<f32>(uv.x, uv.y, uv.x + 1.0, 0.0); out.position = vec4<f32>(0.0, 0.0, 0.0, 1.0); return out; }").expect("owned fallback fixture"),
327 stage: Stage::Vertex, entry: "main".into(), interface,
328 bindings: vec![], conventions: iridia::Conventions::D3D,
329 modes: Default::default(), notes: vec![], provenance: iridia::Provenance {
330 format: iridia_interface::ShaderFormat::Glsl, target: "owned fixture".into(), lifter: "fixture".into(), lifter_version: 1, source_hash: None, instruction_count: None,
331 },
332 };
333 let io = iridia::entry_io(&program.interface).expect("owned fixture interface");
334 let result = program.module.entry_points[0]
335 .function
336 .result
337 .as_ref()
338 .expect("fixture output");
339 let naga::TypeInner::Struct { members, .. } = &program.module.types[result.ty].inner else {
340 panic!("fixture output struct")
341 };
342 assert_eq!(
343 members[0].binding.as_ref(),
344 Some(&io.results[0].binding),
345 "fixture's WGSL interpolation must agree with the IR interface"
346 );
347 let entity = crate::World::new().spawn();
348 let mut vertex = StageBinding::new(Stage::Vertex, entity);
349 vertex.inputs.push(InputBinding {
350 label: program.interface.inputs[0].label.clone(),
351 role: InputRole::Stream(Attribute::TexCoord(0)),
352 });
353 let mut pixel = StageBinding::new(Stage::Pixel, entity);
354 pixel.inputs.push(InputBinding {
355 label: program.interface.outputs[0].label.clone(),
356 role: InputRole::Previous,
357 });
358 assert_eq!(
359 bind_fallback_uv_copies(&mut pixel, &program, &vertex, base_coordinates()),
360 1
361 );
362 let InputRole::PreviousOr { fallback, .. } = &pixel.inputs[0].role else {
363 panic!("copy description")
364 };
365 assert_eq!(
366 fallback
367 .coordinate_copies
368 .as_ref()
369 .expect("owned fallback fixture")
370 .lanes,
371 [Some(0), Some(1), None, None]
372 );
373 vertex.inputs[0].role = InputRole::Unknown {
374 why: "not stated to be UVs".into(),
375 };
376 pixel.inputs[0].role = InputRole::Previous;
377 assert_eq!(
378 bind_fallback_uv_copies(&mut pixel, &program, &vertex, base_coordinates()),
379 0
380 );
381 assert_eq!(pixel.inputs[0].role, InputRole::Previous);
382 }
383}