use iridia::flow::{Node, Values};
use iridia_interface::{Interface, ScalarType, Stage};
use serde::Serialize;
use super::{Attribute, InputRole, StageBinding};
use crate::pbr::TextureSlot;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct FallbackCoordinates {
pub slot: TextureSlot,
pub absent_texture_set: u8,
pub absent_stream: [u32; 2],
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct FallbackCoordinateCopies {
pub coordinates: FallbackCoordinates,
pub lanes: [Option<u8>; 4],
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InputFallbackValue {
Normal,
MaterialVertexColour { absent: [u32; 4] },
MaterialCoordinates(FallbackCoordinates),
Literal([u32; 4]),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct InputFallback {
pub value: InputFallbackValue,
pub coordinate_copies: Option<FallbackCoordinateCopies>,
}
impl InputFallback {
pub fn new(value: InputFallbackValue) -> Self {
Self {
value,
coordinate_copies: None,
}
}
pub fn check(&self) -> Result<(), String> {
let finite = |words: &[u32]| {
if words.iter().all(|word| f32::from_bits(*word).is_finite()) {
Ok(())
} else {
Err("replacement input contains a non-finite float word".to_owned())
}
};
match &self.value {
InputFallbackValue::Normal => {}
InputFallbackValue::MaterialVertexColour { absent } | InputFallbackValue::Literal(absent) => {
finite(absent)?
}
InputFallbackValue::MaterialCoordinates(coordinates) => finite(&coordinates.absent_stream)?,
}
if let Some(copies) = &self.coordinate_copies {
finite(&copies.coordinates.absent_stream)?;
if copies.lanes.iter().flatten().any(|lane| *lane > 1) {
return Err("replacement coordinate-copy lane is neither u nor v".to_owned());
}
}
Ok(())
}
}
fn base_coordinates() -> FallbackCoordinates {
FallbackCoordinates {
slot: TextureSlot::BaseColor,
absent_texture_set: 0,
absent_stream: [0; 2],
}
}
pub fn bind_surface_input_fallbacks(stage: &mut StageBinding, interface: &Interface) -> usize {
if stage.stage != Stage::Pixel || interface.stage != Stage::Pixel {
return 0;
}
let mut count = 0;
for input in &mut stage.inputs {
if !matches!(input.role, InputRole::Previous | InputRole::Unknown { .. }) {
continue;
}
let Some(varying) = interface.inputs.iter().find(|v| v.label == input.label) else {
continue;
};
if varying.system.is_some() || varying.value.scalar != ScalarType::Float || input.label.index != 0 {
continue;
}
let (value, why) = match input.label.name.to_ascii_uppercase().as_str() {
"NORMAL" => (InputFallbackValue::Normal, "the mesh normal"),
"COLOR" => (
InputFallbackValue::MaterialVertexColour {
absent: [1.0f32.to_bits(); 4],
},
"the mesh vertex colour",
),
"TEXCOORD" => (
InputFallbackValue::MaterialCoordinates(base_coordinates()),
"the mesh UV, in both pairs",
),
"TANGENT" => (
InputFallbackValue::Literal([1.0f32.to_bits(), 0, 0, 1.0f32.to_bits()]),
"stand-in: the +X tangent (the scene has none)",
),
_ => continue,
};
input.role = InputRole::PreviousOr {
fallback: InputFallback::new(value),
why: why.into(),
};
count += 1;
}
count
}
pub fn bind_fallback_uv_copies(
pixel: &mut StageBinding,
program: &iridia::Program,
vertex: &StageBinding,
coordinates: FallbackCoordinates,
) -> usize {
if pixel.stage != Stage::Pixel || vertex.stage != Stage::Vertex || program.stage != Stage::Vertex {
return 0;
}
let Ok(values) = Values::of_program(program) else {
return 0;
};
let mut copies = std::collections::BTreeMap::new();
for (&(output, lane), &id) in values.outputs() {
let out = &program.interface.outputs[output];
let Node::Input { input, lane: from } = values.node(id) else {
continue;
};
let source = &program.interface.inputs[*input];
let is_uv = vertex
.inputs
.iter()
.any(|role| role.label == source.label && role.role == InputRole::Stream(Attribute::TexCoord(0)));
if out.system.is_none()
&& out.value.scalar == ScalarType::Float
&& source.value.scalar == ScalarType::Float
&& *from < 2
&& is_uv
{
copies.insert(
(out.label.name.to_ascii_uppercase(), out.label.index, lane),
*from,
);
}
}
let mut count = 0;
for input in &mut pixel.inputs {
let lanes = std::array::from_fn(|lane| {
copies
.get(&(
input.label.name.to_ascii_uppercase(),
input.label.index,
lane as u8,
))
.copied()
});
if lanes.iter().all(Option::is_none) {
continue;
}
match &mut input.role {
InputRole::PreviousOr { fallback, .. } => {
fallback.coordinate_copies = Some(FallbackCoordinateCopies {
coordinates: coordinates.clone(),
lanes,
});
}
InputRole::Previous | InputRole::Unknown { .. } => {
input.role = InputRole::PreviousOr {
fallback: InputFallback {
value: InputFallbackValue::Literal([0; 4]),
coordinate_copies: Some(FallbackCoordinateCopies {
coordinates: coordinates.clone(),
lanes,
}),
},
why: "stand-in: zero (the scene has no such stream)".into(),
};
}
_ => continue,
}
count += 1;
}
count
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shader::InputBinding;
use iridia_interface::{Label, Value, Varying, VaryingProvenance};
fn varying(name: &str, index: u32, scalar: ScalarType, width: u8) -> Varying {
Varying {
label: Label {
name: name.into(),
index,
},
system: None,
value: Value { scalar, width },
interp: None,
used: true,
provenance: VaryingProvenance::default(),
}
}
#[test]
fn standard_surface_fallbacks_are_explicit_and_leave_other_labels_and_roles_alone() {
let interface = Interface {
stage: Stage::Pixel,
inputs: vec![
varying("NORMAL", 0, ScalarType::Float, 3),
varying("COLOR", 0, ScalarType::Float, 4),
varying("TEXCOORD", 0, ScalarType::Float, 4),
varying("TANGENT", 0, ScalarType::Float, 4),
varying("TEXCOORD", 7, ScalarType::Float, 4),
varying("NORMAL", 1, ScalarType::Float, 3),
varying("TANGENT", 1, ScalarType::Uint, 4),
],
outputs: vec![],
resources: vec![],
facts: Default::default(),
};
let mut stage = StageBinding::new(Stage::Pixel, crate::World::new().spawn());
stage.inputs = interface
.inputs
.iter()
.map(|v| InputBinding {
label: v.label.clone(),
role: InputRole::Previous,
})
.collect();
assert_eq!(bind_surface_input_fallbacks(&mut stage, &interface), 4);
for input in &stage.inputs[..4] {
let InputRole::PreviousOr { fallback, why } = &input.role else {
panic!("explicit fallback")
};
assert!(!why.is_empty());
assert!(fallback.check().is_ok());
}
assert!(
stage.inputs[4..]
.iter()
.all(|input| input.role == InputRole::Previous)
);
assert_eq!(
bind_surface_input_fallbacks(&mut stage, &interface),
0,
"idempotent"
);
stage.inputs[0].role = InputRole::Stream(Attribute::Normal);
assert_eq!(bind_surface_input_fallbacks(&mut stage, &interface), 0);
assert_eq!(stage.inputs[0].role, InputRole::Stream(Attribute::Normal));
}
#[test]
fn fallback_words_and_copy_masks_refuse_nonfinite_or_undefined_components() {
let mut fallback = InputFallback::new(InputFallbackValue::Literal([0; 4]));
assert!(fallback.check().is_ok());
fallback.value = InputFallbackValue::Literal([f32::NAN.to_bits(); 4]);
assert!(fallback.check().is_err());
fallback.value = InputFallbackValue::MaterialVertexColour {
absent: [f32::INFINITY.to_bits(); 4],
};
assert!(fallback.check().is_err());
fallback.value = InputFallbackValue::MaterialCoordinates(base_coordinates());
fallback.coordinate_copies = Some(FallbackCoordinateCopies {
coordinates: base_coordinates(),
lanes: [Some(2), None, None, None],
});
assert!(fallback.check().is_err());
fallback
.coordinate_copies
.as_mut()
.expect("owned fallback fixture")
.lanes[0] = Some(1);
assert!(fallback.check().is_ok());
}
#[test]
fn replacement_copy_masks_follow_only_bare_uv_lanes_in_the_vertex_program() {
let mut interface = Interface {
stage: Stage::Vertex,
inputs: vec![varying("packed_uv", 0, ScalarType::Float, 2)],
outputs: vec![varying("custom_varying", 2, ScalarType::Float, 4)],
resources: vec![],
facts: Default::default(),
};
let mut position = varying("position", 0, ScalarType::Float, 4);
position.system = Some(iridia_interface::SystemValue::ClipPosition);
interface.outputs.push(position);
let program = iridia::Program {
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"),
stage: Stage::Vertex, entry: "main".into(), interface,
bindings: vec![], conventions: iridia::Conventions::D3D,
modes: Default::default(), notes: vec![], provenance: iridia::Provenance {
format: iridia_interface::ShaderFormat::Glsl, target: "owned fixture".into(), lifter: "fixture".into(), lifter_version: 1, source_hash: None, instruction_count: None,
},
};
let io = iridia::entry_io(&program.interface).expect("owned fixture interface");
let result = program.module.entry_points[0]
.function
.result
.as_ref()
.expect("fixture output");
let naga::TypeInner::Struct { members, .. } = &program.module.types[result.ty].inner else {
panic!("fixture output struct")
};
assert_eq!(
members[0].binding.as_ref(),
Some(&io.results[0].binding),
"fixture's WGSL interpolation must agree with the IR interface"
);
let entity = crate::World::new().spawn();
let mut vertex = StageBinding::new(Stage::Vertex, entity);
vertex.inputs.push(InputBinding {
label: program.interface.inputs[0].label.clone(),
role: InputRole::Stream(Attribute::TexCoord(0)),
});
let mut pixel = StageBinding::new(Stage::Pixel, entity);
pixel.inputs.push(InputBinding {
label: program.interface.outputs[0].label.clone(),
role: InputRole::Previous,
});
assert_eq!(
bind_fallback_uv_copies(&mut pixel, &program, &vertex, base_coordinates()),
1
);
let InputRole::PreviousOr { fallback, .. } = &pixel.inputs[0].role else {
panic!("copy description")
};
assert_eq!(
fallback
.coordinate_copies
.as_ref()
.expect("owned fallback fixture")
.lanes,
[Some(0), Some(1), None, None]
);
vertex.inputs[0].role = InputRole::Unknown {
why: "not stated to be UVs".into(),
};
pixel.inputs[0].role = InputRole::Previous;
assert_eq!(
bind_fallback_uv_copies(&mut pixel, &program, &vertex, base_coordinates()),
0
);
assert_eq!(pixel.inputs[0].role, InputRole::Previous);
}
}