Skip to main content

primase_core/shader/
input_fallback.rs

1//! Source-stated replacement inputs when a pixel program has no executable previous stage. Standard surface
2//! semantics are shared machinery; a source binding pass explicitly opts in and owns any further label policy.
3
4use 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/// Coordinates resolved through a drawn material's PBR texture reference, distinct from a raw mesh UV stream.
12/// Read the reference's UV set and apply its transform once per vertex, in the transform's stored order. With
13/// no reference use `absent_texture_set` with no transform; with no such stream use `absent_stream` float words.
14#[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/// Lanes whose preceding vertex program only copies a coordinate component. `Some(0)` is u, `Some(1)` is v;
22/// `None` keeps the base fallback's lane. The source binding states the coordinate interpretation explicitly.
23#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
24pub struct FallbackCoordinateCopies {
25    pub coordinates: FallbackCoordinates,
26    pub lanes: [Option<u8>; 4],
27}
28
29/// The base value of a replacement input, before any source-proved coordinate copies.
30#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum InputFallbackValue {
33    /// The drawn surface's normalized world normal, xyz, with w=0. Generic geometry normal generation is separate.
34    Normal,
35    /// COLOR_0 as surface colour, honoring `core.vertex_color`. Ignored or absent colour uses the stated float
36    /// words; the PBR base-colour factor is not multiplied into this value. Raw `Stream(Color(0))` ignores that policy.
37    MaterialVertexColour { absent: [u32; 4] },
38    /// The material-resolved coordinate pair repeated as xyxy. Raw `Stream(TexCoord(n))` applies no PBR transform.
39    MaterialCoordinates(FallbackCoordinates),
40    /// Four finite IEEE-754 float words, stated by the source, not invented by the consumer.
41    Literal([u32; 4]),
42}
43
44/// One source-stated input used only when a preceding stage cannot execute. The four base value forms cover
45/// normals, material colour, material coordinates and literals; optional copies retain the vertex program's
46/// proved lane wiring. A consumer with no description supplies zero and names the missing input in its report.
47#[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    /// State a base value with no coordinate-copy lanes.
55    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
95/// Opt a registered binding pass into standard surface-semantic replacement inputs. Only NORMAL0, COLOR0,
96/// TEXCOORD0 and TANGENT0 float varyings whose role is Previous/Unknown are changed; explicit roles, system inputs
97/// and every other label stay untouched. Returns the number stated so the calling pass can report it by name.
98/// This is a compatibility approximation for a missing/unusable vertex stage, never a native shader inference.
99pub 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
141/// Retain the bare UV-copy lanes of a preceding vertex program in replacement descriptions. A coordinate lane
142/// is proved only when its value graph is a direct input lane and that input's role is raw TexCoord(0). The
143/// replacement uses the explicitly stated coordinate interpretation `coordinates`, not a consumer's UV policy.
144/// Arithmetic, non-float outputs and unresolved programs produce no guessed copies. Returns inputs augmented.
145pub 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}