refactor: order raster cohorts by authored nodes
Amp-Thread-ID: https://ampcode.com/threads/T-019ff1fa-611d-71c8-aefd-86659bff2075 Co-authored-by: Akash Shakdwipeea <ashakdwipeea@gmail.com>
This commit is contained in:
@@ -22,7 +22,6 @@ struct CullParameters {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct RasterParameters {
|
||||
draw_order: i32,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
@@ -770,7 +769,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
));
|
||||
}
|
||||
NormalizedParameters::Raster {
|
||||
draw_order: p.draw_order,
|
||||
depth_compare: p.depth_compare,
|
||||
depth_write_enabled: p.depth_write_enabled,
|
||||
clear_depth: p.clear_depth,
|
||||
@@ -1138,33 +1136,43 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve authored, non-normalized dataflow before attachment normalization. It
|
||||
// is used both to diagnose impossible draw-order normalization and to find
|
||||
// readers which must finish before an attachment version is overwritten.
|
||||
let ordinary_edges: Vec<_> = edges
|
||||
.iter()
|
||||
.filter(|edge| !is_normalizable_target(edge))
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut target_consumers = HashMap::<OutputKey, Vec<usize>>::new();
|
||||
// Preserve authored readers that must finish before an attachment version is overwritten.
|
||||
let mut ordinary_consumers = HashMap::<OutputKey, Vec<usize>>::new();
|
||||
for edge in &edges {
|
||||
let key = OutputKey(edge.from_node, edge.producer_output_ordinal);
|
||||
if is_normalizable_target(edge) {
|
||||
target_consumers.entry(key).or_default().push(edge.to_node);
|
||||
} else if is_exact_reader(edge) && provisional_live.contains(&edge.to_node) {
|
||||
if !is_normalizable_target(edge)
|
||||
&& is_exact_reader(edge)
|
||||
&& provisional_live.contains(&edge.to_node)
|
||||
{
|
||||
ordinary_consumers
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(edge.to_node);
|
||||
}
|
||||
}
|
||||
let observation_cuts: HashSet<_> = target_consumers
|
||||
.keys()
|
||||
.filter(|key| ordinary_consumers.contains_key(key))
|
||||
.copied()
|
||||
let authored_depends_on = |consumer: usize, dependency: usize| {
|
||||
let mut seen = vec![false; graph.nodes.len()];
|
||||
let mut pending = vec![consumer];
|
||||
while let Some(node) = pending.pop() {
|
||||
if node == dependency {
|
||||
return true;
|
||||
}
|
||||
if !seen[node] {
|
||||
seen[node] = true;
|
||||
pending.extend(authored_deps[node].iter().copied());
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
// Attachment normalization may replace an authored raster-to-raster edge,
|
||||
// but it must not erase the ordering constraint expressed by that edge.
|
||||
// A constraint opposite to authored cohort order is therefore reported by
|
||||
// the ordinary cycle detector below.
|
||||
let mut ordering_edges: Vec<_> = edges
|
||||
.iter()
|
||||
.filter(|edge| is_normalizable_target(edge))
|
||||
.map(|edge| (edge.from_node, edge.to_node))
|
||||
.collect();
|
||||
let mut ordering_edges = Vec::new();
|
||||
|
||||
// Normalize raster attachment versions before liveness. Authored graphs may
|
||||
// express a render-pass cohort either as a chain or as direct siblings of
|
||||
@@ -1188,11 +1196,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
));
|
||||
}
|
||||
colors.insert((node, ordinal), 1);
|
||||
let socket = if ordinal == 0 {
|
||||
"colorTarget"
|
||||
} else {
|
||||
"depthTarget"
|
||||
};
|
||||
let socket = if ordinal == 0 { "color" } else { "depth" };
|
||||
let root = match bound[node].get(socket).map(|binding| binding.producer) {
|
||||
None => AttachmentRoot::CompilerDefault { node, ordinal },
|
||||
Some(output)
|
||||
@@ -1233,18 +1237,21 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
cohorts.entry(root).or_default().push(node);
|
||||
}
|
||||
for (root, mut members) in cohorts {
|
||||
members.sort_by_key(|&node| {
|
||||
let NormalizedParameters::Raster { draw_order, .. } = params[node] else {
|
||||
unreachable!()
|
||||
};
|
||||
(draw_order, node)
|
||||
});
|
||||
members.sort_unstable();
|
||||
// An observed version ends its aliasing segment. Writers after the
|
||||
// cut continue the same physical lineage, but may not replace the
|
||||
// version seen by the reader.
|
||||
let mut segment_start = 0;
|
||||
for position in 0..members.len() {
|
||||
let at_cut = observation_cuts.contains(&OutputKey(members[position], ordinal));
|
||||
let output = OutputKey(members[position], ordinal);
|
||||
let at_cut = members.get(position + 1).is_some_and(|&next_writer| {
|
||||
ordinary_consumers.get(&output).is_some_and(|readers| {
|
||||
readers.iter().any(|&reader| {
|
||||
contracts[reader].fullscreen_policy.is_some()
|
||||
&& !authored_depends_on(reader, next_writer)
|
||||
})
|
||||
})
|
||||
});
|
||||
if at_cut || position + 1 == members.len() {
|
||||
let terminal = OutputKey(members[position], ordinal);
|
||||
for &member in &members[segment_start..=position] {
|
||||
@@ -1263,11 +1270,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
segment_start = position + 1;
|
||||
}
|
||||
}
|
||||
let socket = if ordinal == 0 {
|
||||
"colorTarget"
|
||||
} else {
|
||||
"depthTarget"
|
||||
};
|
||||
let socket = if ordinal == 0 { "color" } else { "depth" };
|
||||
for (position, &member) in members.iter().enumerate() {
|
||||
let target = if position == 0 {
|
||||
match root {
|
||||
@@ -1290,10 +1293,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
// and redirect observations of any cohort member to its terminal version.
|
||||
edges.retain(|edge| {
|
||||
!(contracts[edge.to_node].is_raster_draw()
|
||||
&& (edge.to_socket == "colorTarget" || edge.to_socket == "depthTarget"))
|
||||
&& (edge.to_socket == "color" || edge.to_socket == "depth"))
|
||||
});
|
||||
for &node in &raster_nodes {
|
||||
for (socket, ordinal) in [("colorTarget", 0u16), ("depthTarget", 1u16)] {
|
||||
for (socket, ordinal) in [("color", 0u16), ("depth", 1u16)] {
|
||||
let Some(binding) = bound[node].get(socket) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1386,42 +1389,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
stack.extend(deps[i].iter().copied());
|
||||
}
|
||||
}
|
||||
// A canonical attachment writer edge may not reverse authored ordinary
|
||||
// dataflow. Keep this distinct from generic cycle reporting so authors get
|
||||
// the draw-order parameter which made normalization impossible.
|
||||
let mut ordinary_outgoing = vec![Vec::new(); graph.nodes.len()];
|
||||
for edge in &ordinary_edges {
|
||||
ordinary_outgoing[edge.from_node].push(edge.to_node);
|
||||
}
|
||||
let ordinary_reaches = |from: usize, to: usize| {
|
||||
let mut seen = vec![false; graph.nodes.len()];
|
||||
let mut pending = vec![from];
|
||||
while let Some(node) = pending.pop() {
|
||||
if node == to {
|
||||
return true;
|
||||
}
|
||||
if !seen[node] {
|
||||
seen[node] = true;
|
||||
pending.extend(ordinary_outgoing[node].iter().copied());
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
let mut draw_order_conflicts: Vec<_> = edges
|
||||
.iter()
|
||||
.filter(|edge| {
|
||||
contracts[edge.from_node].is_raster_draw()
|
||||
&& contracts[edge.to_node].is_raster_draw()
|
||||
&& matches!(
|
||||
contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize].role,
|
||||
InputRole::ColorTarget { .. } | InputRole::DepthTarget
|
||||
)
|
||||
&& ordinary_reaches(edge.to_node, edge.from_node)
|
||||
})
|
||||
.map(|edge| edge.to_node)
|
||||
.collect();
|
||||
draw_order_conflicts.sort_unstable();
|
||||
draw_order_conflicts.dedup();
|
||||
// IDs are independent of scheduling: original node order, then contract output order.
|
||||
// Source nodes expose only outputs that survived active-edge/liveness analysis;
|
||||
// executable nodes retain their complete output shape for runtime lowering.
|
||||
@@ -1520,16 +1487,16 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
for (socket, role, format, opposite) in [
|
||||
(
|
||||
"colorTarget",
|
||||
"color",
|
||||
CompilerTextureRole::ColorTarget,
|
||||
TextureFormat::Rgba16Float,
|
||||
"depthTarget",
|
||||
"depth",
|
||||
),
|
||||
(
|
||||
"depthTarget",
|
||||
"depth",
|
||||
CompilerTextureRole::DepthTarget,
|
||||
TextureFormat::Depth32Float,
|
||||
"colorTarget",
|
||||
"color",
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
@@ -1572,7 +1539,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
continue;
|
||||
}
|
||||
let transition_sockets: &[(&str, u16)] = match contracts[i] {
|
||||
ref contract if contract.is_raster_draw() => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||
ref contract if contract.is_raster_draw() => &[("color", 0), ("depth", 1)],
|
||||
_ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)],
|
||||
_ => continue,
|
||||
};
|
||||
@@ -1715,9 +1682,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
continue;
|
||||
}
|
||||
if contracts[i].is_raster_draw() {
|
||||
if bound[i].get("colorTarget").map(|b| b.producer)
|
||||
== bound[i].get("depthTarget").map(|b| b.producer)
|
||||
&& bound[i].contains_key("colorTarget")
|
||||
if bound[i].get("color").map(|b| b.producer)
|
||||
== bound[i].get("depth").map(|b| b.producer)
|
||||
&& bound[i].contains_key("color")
|
||||
|| matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df)
|
||||
{
|
||||
return Err(error(
|
||||
@@ -1927,14 +1894,14 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"color attachment is invalid",
|
||||
format!("nodes[{i}].inputs.colorTarget"),
|
||||
format!("nodes[{i}].inputs.color"),
|
||||
));
|
||||
}
|
||||
if !ok_depth {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"depth attachment is invalid",
|
||||
format!("nodes[{i}].inputs.depthTarget"),
|
||||
format!("nodes[{i}].inputs.depth"),
|
||||
));
|
||||
}
|
||||
if let (Some(cd), Some(dd)) = (cd, dd) {
|
||||
@@ -2091,13 +2058,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
|
||||
// Stable Kahn scheduling is deliberately after resource and access validation.
|
||||
if let Some(&node) = draw_order_conflicts.iter().find(|node| live.contains(node)) {
|
||||
return Err(error(
|
||||
"GRAPH_DRAW_ORDER_CONFLICT",
|
||||
"draw order conflicts with authored dependencies",
|
||||
format!("nodes[{node}].parameters.drawOrder"),
|
||||
));
|
||||
}
|
||||
let mut indegree = vec![0; graph.nodes.len()];
|
||||
for &node in &live {
|
||||
indegree[node] = edges
|
||||
@@ -3150,10 +3110,10 @@ pub(crate) fn build_render_passes(
|
||||
.map(|input| input.resource)
|
||||
};
|
||||
let exact = previous.outputs.iter().any(|out| {
|
||||
out.socket == "color" && Some(out.resource) == target_input("colorTarget")
|
||||
out.socket == "color" && Some(out.resource) == target_input("color")
|
||||
}) && depth.as_ref().is_none_or(|_| {
|
||||
previous.outputs.iter().any(|out| {
|
||||
out.socket == "depth" && Some(out.resource) == target_input("depthTarget")
|
||||
out.socket == "depth" && Some(out.resource) == target_input("depth")
|
||||
})
|
||||
});
|
||||
let compatible = previous_colors.len() == colors.len()
|
||||
|
||||
@@ -146,13 +146,8 @@ const TEXTURE_O: &[OutputSocketContract] = &[o("texture", Texture)];
|
||||
const RASTER_I: &[InputSocketContract] = &[
|
||||
i("mesh", MeshData, R, InputRole::SemanticRead),
|
||||
i("predicate", Bool, O, InputRole::Expression),
|
||||
i(
|
||||
"colorTarget",
|
||||
Texture,
|
||||
O,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
i("depthTarget", Texture, O, InputRole::DepthTarget),
|
||||
i("color", Texture, O, InputRole::ColorTarget { location: 0 }),
|
||||
i("depth", Texture, O, InputRole::DepthTarget),
|
||||
];
|
||||
const RASTER_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
|
||||
const CULL_I: &[InputSocketContract] = &[
|
||||
@@ -207,11 +202,11 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
c!("mesh", 2, Source, NONE_I, MESH_O, false, None),
|
||||
c!("texture", 2, Source, NONE_I, TEXTURE_O, false, None),
|
||||
c!("frustum_cull", 2, Expression, CULL_I, CULL_O, false, None),
|
||||
c!("ground_plane", 1, Render, RASTER_I, RASTER_O, false, None),
|
||||
c!("gltf_standard", 1, Render, RASTER_I, RASTER_O, false, None),
|
||||
c!("ground_plane", 2, Render, RASTER_I, RASTER_O, false, None),
|
||||
c!("gltf_standard", 2, Render, RASTER_I, RASTER_O, false, None),
|
||||
c!(
|
||||
"gltf_standard_double_sided",
|
||||
1,
|
||||
2,
|
||||
Render,
|
||||
RASTER_I,
|
||||
RASTER_O,
|
||||
|
||||
@@ -221,7 +221,6 @@ pub enum NormalizedParameters {
|
||||
defaults: Vec<TypedLiteral>,
|
||||
},
|
||||
Raster {
|
||||
draw_order: i32,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
|
||||
@@ -861,6 +861,17 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
format!("renderPasses[{pass_index}]"),
|
||||
));
|
||||
}
|
||||
if matches!(pass.kind, PhysicalRenderPassKind::Texture { .. })
|
||||
&& pass.executions.windows(2).any(|members| {
|
||||
graph.executions[members[0] as usize].original_node_index
|
||||
>= graph.executions[members[1] as usize].original_node_index
|
||||
})
|
||||
{
|
||||
return Err(invalid(
|
||||
"raster pass members are not in authored node order",
|
||||
format!("renderPasses[{pass_index}].executions"),
|
||||
));
|
||||
}
|
||||
}
|
||||
if expected_execution != graph.executions.len() {
|
||||
return Err(invalid(
|
||||
@@ -891,27 +902,6 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
"renderPasses",
|
||||
));
|
||||
}
|
||||
for (pass_index, pass) in graph.render_passes.iter().enumerate() {
|
||||
if !matches!(pass.kind, PhysicalRenderPassKind::Texture { .. }) {
|
||||
continue;
|
||||
}
|
||||
let mut previous = None;
|
||||
for &member in &pass.executions {
|
||||
let execution = &graph.executions[member as usize];
|
||||
let NormalizedParameters::Raster { draw_order, .. } = &execution.parameters else {
|
||||
previous = None;
|
||||
continue;
|
||||
};
|
||||
let key = (*draw_order, execution.original_node_index);
|
||||
if previous.is_some_and(|value| value > key) {
|
||||
return Err(invalid(
|
||||
"raster pass members are not in canonical draw order",
|
||||
format!("renderPasses[{pass_index}].executions"),
|
||||
));
|
||||
}
|
||||
previous = Some(key);
|
||||
}
|
||||
}
|
||||
let execution_pass: Vec<u32> = graph
|
||||
.render_passes
|
||||
.iter()
|
||||
@@ -1020,51 +1010,26 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
let consumer_contract =
|
||||
contract(&execution.executor.key).expect("supported executor has contract");
|
||||
let is_attachment = consumer_contract
|
||||
let producer_execution = &graph.executions[producer as usize];
|
||||
let input_contract = contract(&execution.executor.key)
|
||||
.expect("supported executor has contract")
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|candidate| candidate.name == input.socket)
|
||||
.is_some_and(|candidate| {
|
||||
.find(|candidate| candidate.name == input.socket);
|
||||
if matches!(execution.kind, ExecutionKind::RasterDraw)
|
||||
&& matches!(producer_execution.kind, ExecutionKind::RasterDraw)
|
||||
&& input_contract.is_some_and(|candidate| {
|
||||
matches!(
|
||||
candidate.role,
|
||||
InputRole::ColorTarget { .. } | InputRole::DepthTarget
|
||||
)
|
||||
});
|
||||
let predecessor = &graph.executions[producer as usize];
|
||||
if is_attachment
|
||||
&& matches!(execution.kind, ExecutionKind::RasterDraw)
|
||||
&& matches!(predecessor.kind, ExecutionKind::RasterDraw)
|
||||
})
|
||||
&& producer_execution.original_node_index >= execution.original_node_index
|
||||
{
|
||||
let NormalizedParameters::Raster {
|
||||
draw_order: predecessor_order,
|
||||
..
|
||||
} = predecessor.parameters
|
||||
else {
|
||||
return Err(invalid(
|
||||
"raster predecessor parameters mismatch",
|
||||
format!("executions[{producer}].parameters"),
|
||||
));
|
||||
};
|
||||
let NormalizedParameters::Raster {
|
||||
draw_order: consumer_order,
|
||||
..
|
||||
} = execution.parameters
|
||||
else {
|
||||
return Err(invalid(
|
||||
"raster execution parameters mismatch",
|
||||
format!("executions[{i}].parameters"),
|
||||
));
|
||||
};
|
||||
if (predecessor_order, predecessor.original_node_index)
|
||||
> (consumer_order, execution.original_node_index)
|
||||
{
|
||||
return Err(invalid(
|
||||
"raster attachment predecessors must have canonical draw order",
|
||||
format!("executions[{i}].parameters.drawOrder"),
|
||||
));
|
||||
}
|
||||
return Err(invalid(
|
||||
"raster attachment predecessors must be in authored node order",
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1162,7 +1127,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
})
|
||||
&& contract(&owner_executions[0].executor.key)
|
||||
.is_some_and(Contract::is_raster_draw)
|
||||
&& owner_executions[0].executor.version == 1
|
||||
&& owner_executions[0].executor.version == 2
|
||||
&& graph
|
||||
.executions
|
||||
.iter()
|
||||
@@ -1189,9 +1154,9 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
&& owner_input_ok
|
||||
&& socket
|
||||
== if *role == CompilerTextureRole::ColorTarget {
|
||||
"colorTarget"
|
||||
"color"
|
||||
} else {
|
||||
"depthTarget"
|
||||
"depth"
|
||||
}
|
||||
}
|
||||
(
|
||||
@@ -1270,9 +1235,9 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
)
|
||||
})?;
|
||||
let opposite_socket = if *role == CompilerTextureRole::ColorTarget {
|
||||
"depthTarget"
|
||||
"depth"
|
||||
} else {
|
||||
"colorTarget"
|
||||
"color"
|
||||
};
|
||||
let opposite_resource = owner
|
||||
.inputs
|
||||
@@ -1752,7 +1717,6 @@ pub fn prepare_runtime_plan(
|
||||
continue;
|
||||
}
|
||||
let NormalizedParameters::Raster {
|
||||
draw_order: _,
|
||||
clear_depth,
|
||||
clear_color,
|
||||
..
|
||||
@@ -1801,7 +1765,7 @@ pub fn prepare_runtime_plan(
|
||||
mesh_input.socket.as_str(),
|
||||
color_input.socket.as_str(),
|
||||
depth_input.socket.as_str(),
|
||||
] != ["mesh", "colorTarget", "depthTarget"]
|
||||
] != ["mesh", "color", "depth"]
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline input sockets mismatch",
|
||||
|
||||
@@ -49,9 +49,9 @@ pub(crate) fn full_cull_graph() -> Value {
|
||||
node("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
|
||||
node("class", "and", 2, json!({}),
|
||||
json!({"inputs":[input("bits","bit0")[0].clone(),input("visible","value")[0].clone()]})),
|
||||
node("pipeline", "gltf_standard", 1,
|
||||
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")})),
|
||||
node("pipeline", "gltf_standard", 2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"color":input("color","texture"),"depth":input("depth","texture")})),
|
||||
node("frame", "frame_out", 3,
|
||||
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,
|
||||
"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]}),
|
||||
@@ -69,7 +69,7 @@ fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
||||
"gltf_standard_double_sided",
|
||||
] {
|
||||
let contract = contract(key).unwrap();
|
||||
assert_eq!(contract.version, 1);
|
||||
assert_eq!(contract.version, 2);
|
||||
assert!(contract.is_raster_draw());
|
||||
}
|
||||
assert_eq!(
|
||||
@@ -189,7 +189,7 @@ fn msaa_resolve_can_feed_fullscreen_and_default_depth_inherits_four_samples() {
|
||||
value["nodes"][8]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("depthTarget");
|
||||
.remove("depth");
|
||||
value["nodes"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
@@ -251,7 +251,7 @@ fn msaa_resolve_accepts_default_color_inferred_from_authored_depth() {
|
||||
value["nodes"][8]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("colorTarget");
|
||||
.remove("color");
|
||||
|
||||
let graph = compile_value(value).unwrap();
|
||||
let (resolve_descriptor, source_resource) = graph
|
||||
@@ -368,31 +368,6 @@ fn pipeline_predicate_defaults_true_and_expression_edges_are_validated() {
|
||||
assert_eq!(compile_value(cycle).unwrap_err().code, "GRAPH_CYCLE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_executors_preserve_identity_and_signed_draw_order() {
|
||||
for (key, draw_order) in [
|
||||
("ground_plane", i32::MIN),
|
||||
("gltf_standard", 0),
|
||||
("gltf_standard_double_sided", i32::MAX),
|
||||
] {
|
||||
let mut graph = full_cull_graph();
|
||||
graph["nodes"][8]["executor"] = json!({"key":key,"version":1});
|
||||
graph["nodes"][8]["parameters"]["drawOrder"] = json!(draw_order);
|
||||
let compiled = compile_value(graph).unwrap();
|
||||
let execution = compiled
|
||||
.executions
|
||||
.iter()
|
||||
.find(|e| e.id == "pipeline")
|
||||
.unwrap();
|
||||
assert_eq!(execution.executor.key, key);
|
||||
assert_eq!(execution.executor.version, 1);
|
||||
assert!(matches!(
|
||||
execution.parameters,
|
||||
NormalizedParameters::Raster { draw_order: value, .. } if value == draw_order
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_executors_reject_removed_pipeline_parameter() {
|
||||
let mut graph = full_cull_graph();
|
||||
@@ -402,6 +377,33 @@ fn raster_executors_reject_removed_pipeline_parameter() {
|
||||
assert_eq!(error.details["path"], "nodes[8].parameters");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_contract_rejects_legacy_versions_parameters_and_sockets() {
|
||||
let mut old_version = full_cull_graph();
|
||||
old_version["nodes"][8]["executor"]["version"] = json!(1);
|
||||
let error = compile_value(old_version).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_EXECUTOR_VERSION_UNSUPPORTED");
|
||||
assert_eq!(error.details["path"], "nodes[8].executor.version");
|
||||
|
||||
let removed_parameter = ["draw", "Order"].concat();
|
||||
let mut old_parameter = full_cull_graph();
|
||||
old_parameter["nodes"][8]["parameters"][&removed_parameter] = json!(0);
|
||||
let error = compile_value(old_parameter).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_PARAMETERS_INVALID");
|
||||
assert_eq!(error.details["path"], "nodes[8].parameters");
|
||||
|
||||
for removed_socket in [["color", "Target"], ["depth", "Target"]].map(|parts| parts.concat()) {
|
||||
let mut old_socket = full_cull_graph();
|
||||
old_socket["nodes"][8]["inputs"][&removed_socket] = input("color", "texture");
|
||||
let error = compile_value(old_socket).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_UNKNOWN_SOCKET");
|
||||
assert_eq!(
|
||||
error.details["path"],
|
||||
format!("nodes[8].inputs.{removed_socket}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_generic_pipeline_executor_is_unknown() {
|
||||
let mut graph = full_cull_graph();
|
||||
@@ -420,9 +422,9 @@ fn sibling_raster_writers_form_one_ordered_physical_pass() {
|
||||
node(
|
||||
"sibling",
|
||||
"ground_plane",
|
||||
1,
|
||||
json!({"drawOrder":1,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")}),
|
||||
2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"color":input("color","texture"),"depth":input("depth","texture")}),
|
||||
),
|
||||
);
|
||||
nodes.insert(10, texture("composite", "rgba16_float"));
|
||||
@@ -471,12 +473,12 @@ fn expression_provenance_rejects_cross_mesh_values() {
|
||||
fn implicit_pipeline_graph() -> Value {
|
||||
json!({ "schemaVersion": 3, "graphId": "implicit", "revision": 1, "nodes": [
|
||||
node("mesh", "mesh", 2, json!({}), json!({})),
|
||||
node("first", "ground_plane", 1,
|
||||
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
node("first", "ground_plane", 2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh")})),
|
||||
node("second", "gltf_standard", 1,
|
||||
json!({"drawOrder":1,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"colorTarget":input("first","color"),"depthTarget":input("first","depth")})),
|
||||
node("second", "gltf_standard", 2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"color":input("first","color"),"depth":input("first","depth")})),
|
||||
node("frame", "frame_out", 3,
|
||||
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]}),
|
||||
json!({"color":input("second","color")}))
|
||||
@@ -486,16 +488,15 @@ fn implicit_pipeline_graph() -> Value {
|
||||
fn three_raster_graph() -> Value {
|
||||
let mut graph = full_cull_graph();
|
||||
let nodes = graph["nodes"].as_array_mut().unwrap();
|
||||
nodes[8]["executor"] = json!({"key":"ground_plane","version":1});
|
||||
nodes[8]["parameters"]["drawOrder"] = json!(20);
|
||||
nodes[8]["executor"] = json!({"key":"ground_plane","version":2});
|
||||
nodes.insert(
|
||||
9,
|
||||
node(
|
||||
"standard",
|
||||
"gltf_standard",
|
||||
1,
|
||||
json!({"drawOrder":10,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[1,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"colorTarget":input("pipeline","color"),"depthTarget":input("pipeline","depth")}),
|
||||
2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[1,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"color":input("pipeline","color"),"depth":input("pipeline","depth")}),
|
||||
),
|
||||
);
|
||||
nodes.insert(
|
||||
@@ -503,9 +504,9 @@ fn three_raster_graph() -> Value {
|
||||
node(
|
||||
"double",
|
||||
"gltf_standard_double_sided",
|
||||
1,
|
||||
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":0.5,"clearColor":[0,1,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"colorTarget":input("standard","color"),"depthTarget":input("standard","depth")}),
|
||||
2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":0.5,"clearColor":[0,1,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh"),"color":input("standard","color"),"depth":input("standard","depth")}),
|
||||
),
|
||||
);
|
||||
nodes[11]["inputs"]["color"] = input("double", "color");
|
||||
@@ -515,8 +516,8 @@ fn three_raster_graph() -> Value {
|
||||
fn direct_raster_graph(frame_source: &str) -> Value {
|
||||
let mut graph = three_raster_graph();
|
||||
for node in [8, 9, 10] {
|
||||
graph["nodes"][node]["inputs"]["colorTarget"] = input("color", "texture");
|
||||
graph["nodes"][node]["inputs"]["depthTarget"] = input("depth", "texture");
|
||||
graph["nodes"][node]["inputs"]["color"] = input("color", "texture");
|
||||
graph["nodes"][node]["inputs"]["depth"] = input("depth", "texture");
|
||||
}
|
||||
graph["nodes"][11]["inputs"]["color"] = input(frame_source, "color");
|
||||
graph
|
||||
@@ -533,7 +534,7 @@ fn direct_raster_outputs_all_observe_the_terminal_cohort() {
|
||||
.iter()
|
||||
.map(|execution| execution.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["double", "standard", "pipeline", "frame"]
|
||||
["pipeline", "standard", "double", "frame"]
|
||||
);
|
||||
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
|
||||
let frame_color = match graph.executions[3].kind {
|
||||
@@ -557,11 +558,8 @@ fn direct_raster_outputs_all_observe_the_terminal_cohort() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_draw_order_uses_authored_node_order() {
|
||||
let mut value = direct_raster_graph("pipeline");
|
||||
for node in [8, 9, 10] {
|
||||
value["nodes"][node]["parameters"]["drawOrder"] = json!(7);
|
||||
}
|
||||
fn shared_raster_cohort_uses_authored_node_order() {
|
||||
let value = direct_raster_graph("pipeline");
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert_eq!(
|
||||
graph.executions[..3]
|
||||
@@ -573,6 +571,83 @@ fn equal_draw_order_uses_authored_node_order() {
|
||||
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observed_raster_output_splits_physical_pass() {
|
||||
let mut value = direct_raster_graph("double");
|
||||
let nodes = value["nodes"].as_array_mut().unwrap();
|
||||
nodes.insert(2, texture("reader_target", "rgba16_float"));
|
||||
nodes.insert(3, texture("composite_target", "rgba16_float"));
|
||||
|
||||
let standard = nodes
|
||||
.iter()
|
||||
.position(|node| node["id"] == "standard")
|
||||
.unwrap();
|
||||
nodes.insert(
|
||||
standard,
|
||||
node(
|
||||
"reader",
|
||||
"fullscreen_copy",
|
||||
1,
|
||||
json!({}),
|
||||
json!({
|
||||
"source": input("pipeline", "color"),
|
||||
"colorTarget": input("reader_target", "texture")
|
||||
}),
|
||||
),
|
||||
);
|
||||
let frame = nodes.iter().position(|node| node["id"] == "frame").unwrap();
|
||||
nodes.insert(
|
||||
frame,
|
||||
node(
|
||||
"composite",
|
||||
"bloom_composite",
|
||||
1,
|
||||
json!({"intensity":1.0}),
|
||||
json!({
|
||||
"source": input("double", "color"),
|
||||
"bloom": input("reader", "color"),
|
||||
"colorTarget": input("composite_target", "texture")
|
||||
}),
|
||||
),
|
||||
);
|
||||
let frame = nodes.iter_mut().find(|node| node["id"] == "frame").unwrap();
|
||||
frame["inputs"]["color"] = input("composite", "color");
|
||||
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert_eq!(
|
||||
graph
|
||||
.executions
|
||||
.iter()
|
||||
.map(|execution| execution.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
"pipeline",
|
||||
"reader",
|
||||
"standard",
|
||||
"double",
|
||||
"composite",
|
||||
"frame"
|
||||
]
|
||||
);
|
||||
assert_eq!(graph.render_passes[0].executions, [0]);
|
||||
assert_eq!(graph.render_passes[1].executions, [1]);
|
||||
assert_eq!(graph.render_passes[2].executions, [2, 3]);
|
||||
let pipeline_color = graph.executions[0]
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| output.socket == "color")
|
||||
.unwrap()
|
||||
.resource;
|
||||
let reader_source = graph.executions[1]
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|input| input.socket == "source")
|
||||
.unwrap()
|
||||
.resource;
|
||||
assert_eq!(reader_source, pipeline_color);
|
||||
assert!(validate_activatable(&graph).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dead_fullscreen_attachment_writer_of_observed_raster_output_stays_dead() {
|
||||
let mut value = direct_raster_graph("pipeline");
|
||||
@@ -758,6 +833,14 @@ fn runtime_rejects_noncanonical_physical_passes() {
|
||||
color_attachments[0].resource = first_color;
|
||||
assert_runtime_plan_invalid(&wrong_resource);
|
||||
|
||||
let mut wrong_authored_order = graph.clone();
|
||||
let last_raster = wrong_authored_order.render_passes[0].executions.len() - 1;
|
||||
let last_node_index = wrong_authored_order.executions[last_raster].original_node_index;
|
||||
wrong_authored_order.executions[last_raster].original_node_index =
|
||||
wrong_authored_order.executions[0].original_node_index;
|
||||
wrong_authored_order.executions[0].original_node_index = last_node_index;
|
||||
assert_runtime_plan_invalid(&wrong_authored_order);
|
||||
|
||||
let mut wrong_lifetime = graph;
|
||||
let intermediate = wrong_lifetime.executions[1].outputs[0].resource;
|
||||
wrong_lifetime.resources[intermediate as usize].lifetime = Some(Lifetime {
|
||||
@@ -770,7 +853,7 @@ fn runtime_rejects_noncanonical_physical_passes() {
|
||||
#[test]
|
||||
fn contract_v4_declares_strict_default_policies() {
|
||||
let pipeline = contract("gltf_standard").unwrap();
|
||||
assert_eq!(pipeline.version, 1);
|
||||
assert_eq!(pipeline.version, 2);
|
||||
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
|
||||
assert_eq!(
|
||||
pipeline.inputs[1].default_policy,
|
||||
@@ -853,7 +936,7 @@ fn implicit_chain_is_deterministic_and_loads_successors() {
|
||||
|
||||
#[test]
|
||||
fn one_missing_attachment_copies_authored_opposite_extent() {
|
||||
for missing in ["colorTarget", "depthTarget"] {
|
||||
for missing in ["color", "depth"] {
|
||||
let mut graph = full_cull_graph();
|
||||
graph["nodes"][8]["inputs"]
|
||||
.as_object_mut()
|
||||
@@ -887,7 +970,7 @@ fn default_extent_follows_half_surface_and_prior_default_families() {
|
||||
half["nodes"][8]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("depthTarget");
|
||||
.remove("depth");
|
||||
let compiled = compile_value(half).unwrap();
|
||||
let default = compiled
|
||||
.texture_families
|
||||
@@ -937,12 +1020,12 @@ fn explicit_attachment_diagnostics_are_socket_specific_then_mutual() {
|
||||
let mut color = full_cull_graph();
|
||||
color["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
||||
let error = compile_value(color).unwrap_err();
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs.colorTarget");
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs.color");
|
||||
|
||||
let mut depth = full_cull_graph();
|
||||
depth["nodes"][1]["parameters"]["texture"]["format"] = json!("rgba16_float");
|
||||
let error = compile_value(depth).unwrap_err();
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs.depthTarget");
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs.depth");
|
||||
|
||||
let mut mismatch = full_cull_graph();
|
||||
set_extent_ratio(&mut mismatch["nodes"][1], 1, 2);
|
||||
@@ -953,20 +1036,19 @@ fn explicit_attachment_diagnostics_are_socket_specific_then_mutual() {
|
||||
#[test]
|
||||
fn descriptor_dependency_cycle_keeps_graph_cycle_priority() {
|
||||
let mut graph = implicit_pipeline_graph();
|
||||
graph["nodes"][1]["inputs"]["depthTarget"] = input("second", "depth");
|
||||
graph["nodes"][1]["inputs"]["depth"] = input("second", "depth");
|
||||
graph["nodes"][2]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("depthTarget");
|
||||
// The backwards authored attachment is normalized by draw order.
|
||||
assert!(compile_value(graph).is_ok());
|
||||
.remove("depth");
|
||||
assert_eq!(compile_value(graph).unwrap_err().code, "GRAPH_CYCLE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_attachment_error_precedes_cycle() {
|
||||
let mut graph = full_cull_graph();
|
||||
graph["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
||||
graph["nodes"][8]["inputs"]["depthTarget"] = input("pipeline", "depth");
|
||||
graph["nodes"][8]["inputs"]["depth"] = input("pipeline", "depth");
|
||||
let error = compile_value(graph).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_ATTACHMENT_LINEAGE_INVALID");
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs");
|
||||
@@ -1029,12 +1111,12 @@ fn fullscreen_output_is_a_valid_raster_attachment_root() {
|
||||
node(
|
||||
"later",
|
||||
"ground_plane",
|
||||
1,
|
||||
json!({"drawOrder":1,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
2,
|
||||
json!({"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({
|
||||
"mesh": input("mesh", "mesh"),
|
||||
"colorTarget": input("copy", "color"),
|
||||
"depthTarget": input("pipeline", "depth")
|
||||
"color": input("copy", "color"),
|
||||
"depth": input("pipeline", "depth")
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -1060,11 +1142,11 @@ fn fullscreen_output_is_a_valid_raster_attachment_root() {
|
||||
#[test]
|
||||
fn known_invalid_fullscreen_target_precedes_source_cycle() {
|
||||
let mut graph = implicit_pipeline_graph();
|
||||
graph["nodes"][1]["inputs"]["depthTarget"] = input("second", "depth");
|
||||
graph["nodes"][1]["inputs"]["depth"] = input("second", "depth");
|
||||
graph["nodes"][2]["inputs"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("depthTarget");
|
||||
.remove("depth");
|
||||
graph["nodes"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
@@ -1327,7 +1409,7 @@ fn runtime_rejects_out_of_range_opposite_default_family_without_panicking() {
|
||||
.unwrap()
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|input| input.socket == "depthTarget")
|
||||
.find(|input| input.socket == "depth")
|
||||
.unwrap()
|
||||
.resource;
|
||||
let out_of_range = graph.texture_families.len() as u32;
|
||||
|
||||
@@ -2091,7 +2091,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
})
|
||||
.transpose()?;
|
||||
let NormalizedParameters::Raster {
|
||||
draw_order: _,
|
||||
depth_compare,
|
||||
depth_write_enabled,
|
||||
..
|
||||
|
||||
@@ -334,6 +334,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
sockets.set(s.id, {
|
||||
node: n.id,
|
||||
key: s.key,
|
||||
semanticName: (input ?? descriptor.outputs[s.key]).semanticName ?? s.key,
|
||||
direction,
|
||||
semanticType: input
|
||||
? input.accepted.types[0]
|
||||
@@ -350,7 +351,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
for (const key of Object.keys(descriptor.inputs)) {
|
||||
const authoredDefault = sockets.get(`${n.id}:${key}`).defaultValue;
|
||||
if (authoredDefault)
|
||||
parameters[`${key}Default`] = parameterValue(
|
||||
parameters[`${descriptor.inputs[key].semanticName ?? key}Default`] = parameterValue(
|
||||
authoredDefault,
|
||||
definition.sockets[key].value,
|
||||
n.id,
|
||||
@@ -446,9 +447,9 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
linkSources.set(link.id, linkSource);
|
||||
if (!link.muted) {
|
||||
incoming.set(link.toSocketId, (incoming.get(link.toSocketId) ?? 0) + 1);
|
||||
(nodes.get(to.node).value.inputs[to.key] ??= []).push({
|
||||
(nodes.get(to.node).value.inputs[to.semanticName] ??= []).push({
|
||||
node: from.node,
|
||||
socket: from.key,
|
||||
socket: from.semanticName,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -463,8 +464,9 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
const item = ordered[wireOrdinal];
|
||||
item.value.inputs = Object.fromEntries(
|
||||
Object.keys(descriptors[item.value.executor.key].inputs)
|
||||
.filter((key) => Object.hasOwn(item.value.inputs, key))
|
||||
.map((key) => [key, item.value.inputs[key]]),
|
||||
.map((key) => descriptors[item.value.executor.key].inputs[key].semanticName ?? key)
|
||||
.filter((semanticName) => Object.hasOwn(item.value.inputs, semanticName))
|
||||
.map((semanticName) => [semanticName, item.value.inputs[semanticName]]),
|
||||
);
|
||||
const base = `nodes[${wireOrdinal}]`;
|
||||
const nodeSource = { kind: "node", nodeId: item.value.id };
|
||||
@@ -561,12 +563,13 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
socketId: `${item.value.id}:${key}`,
|
||||
unconnected: true,
|
||||
};
|
||||
paths[`${base}.inputs.${key}`] = source;
|
||||
const semanticName = descriptors[item.value.executor.key].inputs[key].semanticName ?? key;
|
||||
paths[`${base}.inputs.${semanticName}`] = source;
|
||||
for (const [index, link] of links.entries()) {
|
||||
const linkSource = linkSources.get(link.id);
|
||||
paths[`${base}.inputs.${key}[${index}]`] = linkSource;
|
||||
paths[`${base}.inputs.${key}[${index}].node`] = linkSource;
|
||||
paths[`${base}.inputs.${key}[${index}].socket`] = {
|
||||
paths[`${base}.inputs.${semanticName}[${index}]`] = linkSource;
|
||||
paths[`${base}.inputs.${semanticName}[${index}].node`] = linkSource;
|
||||
paths[`${base}.inputs.${semanticName}[${index}].socket`] = {
|
||||
kind: "socket",
|
||||
nodeId: link.fromNodeId,
|
||||
socketId: link.fromSocketId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const GRAPH_ID = "authored_gpu_culling";
|
||||
export const CATALOG_VERSION = 12;
|
||||
export const CATALOG_VERSION = 13;
|
||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||
const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" : "parameter_literal", maximum = 1) => ({
|
||||
accepted: typeof type === "string" ? exact(type) : type,
|
||||
@@ -7,7 +7,7 @@ const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" :
|
||||
...(authoringType ? { authoringType } : {}),
|
||||
defaultPolicy,
|
||||
});
|
||||
const o = (type) => ({ type });
|
||||
const o = (type, semanticName) => ({ type, ...(semanticName ? { semanticName } : {}) });
|
||||
const expression = (inputs, outputs) => ({
|
||||
version: 1,
|
||||
execution: "expression",
|
||||
@@ -66,12 +66,12 @@ const texture = {
|
||||
const rasterInputs = () => ({
|
||||
mesh: i("mesh_data"),
|
||||
predicate: i("bool", 0),
|
||||
colorTarget: i("texture", 0, undefined, "compiler_texture"),
|
||||
depthTarget: i("texture", 0, undefined, "compiler_texture"),
|
||||
"input.color": { ...i("texture", 0, undefined, "compiler_texture"), semanticName: "color" },
|
||||
"input.depth": { ...i("texture", 0, undefined, "compiler_texture"), semanticName: "depth" },
|
||||
});
|
||||
const rasterOutputs = () => ({ color: o("texture"), depth: o("texture") });
|
||||
const rasterParameters = () => ({ drawOrder: 0, depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] });
|
||||
const raster = () => ({ version: 1, execution: "render", inputs: rasterInputs(), outputs: rasterOutputs(), parameters: rasterParameters() });
|
||||
const rasterOutputs = () => ({ "output.color": o("texture", "color"), "output.depth": o("texture", "depth") });
|
||||
const rasterParameters = () => ({ depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] });
|
||||
const raster = () => ({ version: 2, execution: "render", inputs: rasterInputs(), outputs: rasterOutputs(), parameters: rasterParameters() });
|
||||
export const NODE_TITLE_OVERRIDES = Object.freeze({
|
||||
ground_plane: "Ground Plane",
|
||||
gltf_standard: "glTF Standard",
|
||||
@@ -379,7 +379,6 @@ const parameterSchemas = {
|
||||
mesh: {},
|
||||
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
||||
ground_plane: {
|
||||
drawOrder: { ...number(0, -2147483648, 2147483647), integer: true },
|
||||
depthCompare: enumeration("less_equal", [
|
||||
"never",
|
||||
"less",
|
||||
@@ -431,7 +430,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
Object.entries(c.inputs).map(([n, v]) => [
|
||||
n,
|
||||
socket(
|
||||
n,
|
||||
v.semanticName ?? n,
|
||||
"input",
|
||||
v.authoringType ?? v.accepted.types[0],
|
||||
v.defaultPolicy === "parameter_literal" ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
|
||||
@@ -442,7 +441,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
...Object.fromEntries(
|
||||
Object.entries(c.outputs).map(([n, v]) => [
|
||||
n,
|
||||
socket(n, "output", v.authoringType ?? v.type),
|
||||
socket(v.semanticName ?? n, "output", v.authoringType ?? v.type),
|
||||
]),
|
||||
),
|
||||
},
|
||||
@@ -483,7 +482,6 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
nodeDefinitions.mesh.sockets.localAabb.title = "Local AABB";
|
||||
for (const key of Object.keys(NODE_TITLE_OVERRIDES)) {
|
||||
for (const item of nodeDefinitions[key].ui) {
|
||||
if (item.parameter === "drawOrder") item.title = "Draw Order";
|
||||
if (item.parameter === "clearColor") item.title = "Initial Color";
|
||||
if (item.parameter === "clearDepth") item.title = "Initial Depth";
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1
|
||||
node("scene_depth", "texture", texture("depth32_float", 1, heightScale, sampleCount)),
|
||||
node("mesh", "mesh"),
|
||||
...classification.nodes,
|
||||
node("ground", "ground_plane", { drawOrder: 0, depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), colorTarget: input(target, "texture"), depthTarget: input("scene_depth", "texture") }),
|
||||
node("pbr", "gltf_standard", { drawOrder: 1, depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), colorTarget: input(target, "texture"), depthTarget: input("scene_depth", "texture") }),
|
||||
node("pbr_double", "gltf_standard_double_sided", { drawOrder: 2, depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), colorTarget: input(target, "texture"), depthTarget: input("scene_depth", "texture") }),
|
||||
node("ground", "ground_plane", { depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), color: input(target, "texture"), depth: input("scene_depth", "texture") }),
|
||||
node("pbr", "gltf_standard", { depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), color: input(target, "texture"), depth: input("scene_depth", "texture") }),
|
||||
node("pbr_double", "gltf_standard_double_sided", { depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), color: input(target, "texture"), depth: input("scene_depth", "texture") }),
|
||||
];
|
||||
};
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 3, graphId, revision: 3, nodes });
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 3, graphId, revision: 4, nodes });
|
||||
const direct = (graphId, clearColor) => graph(graphId, [
|
||||
node("ldr", "texture", texture("rgba8_unorm")),
|
||||
...scene("ldr", clearColor),
|
||||
|
||||
@@ -36,7 +36,7 @@ test("production render graph composition passes fxnode's public validator", asy
|
||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||
);
|
||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||
assert.equal(fxNodeComposition.version, 12);
|
||||
assert.equal(fxNodeComposition.version, 13);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 44);
|
||||
assert.ok(
|
||||
Object.values(fxNodeComposition.nodes).every(
|
||||
|
||||
@@ -30,8 +30,8 @@ const authoredLink = (id, from, to = "target", muted = false) => ({
|
||||
toNodeId: to, toSocketId: `${to}:inputs`, muted, extensions: {},
|
||||
});
|
||||
|
||||
test("catalog v12 exposes raster and typed-expression contracts", () => {
|
||||
assert.equal(CATALOG_VERSION, 12);
|
||||
test("catalog v13 exposes raster and typed-expression contracts", () => {
|
||||
assert.equal(CATALOG_VERSION, 13);
|
||||
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
||||
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
||||
});
|
||||
@@ -39,7 +39,7 @@ test("catalog v12 exposes raster and typed-expression contracts", () => {
|
||||
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
||||
assert.equal(semanticCatalog.pipeline, undefined);
|
||||
for (const key of ["ground_plane", "gltf_standard", "gltf_standard_double_sided"]) {
|
||||
assert.equal(semanticCatalog[key].version, 1);
|
||||
assert.equal(semanticCatalog[key].version, 2);
|
||||
assert.deepEqual(semanticCatalog[key].inputs.predicate.cardinality, { minimum: 0, maximum: 1 });
|
||||
}
|
||||
assert.deepEqual(semanticCatalog.and.inputs.inputs.cardinality, { minimum: 0, maximum: 8 });
|
||||
@@ -66,18 +66,34 @@ test("raster declarations own their defaults and sockets", () => {
|
||||
assert.notStrictEqual(nodeDefinitions.ground_plane.parameters.clearColor.default.value,
|
||||
nodeDefinitions.gltf_standard.parameters.clearColor.default.value);
|
||||
assert.equal("pipeline" in nodeDefinitions.gltf_standard.parameters, false);
|
||||
assert.equal(["draw", "Order"].join("") in nodeDefinitions.gltf_standard.parameters, false);
|
||||
assert.deepEqual(Object.keys(semanticCatalog.gltf_standard.inputs), [
|
||||
"mesh", "predicate", "input.color", "input.depth",
|
||||
]);
|
||||
assert.deepEqual(Object.keys(semanticCatalog.gltf_standard.outputs), [
|
||||
"output.color", "output.depth",
|
||||
]);
|
||||
for (const [identity, direction, semanticName] of [
|
||||
["input.color", "input", "color"],
|
||||
["output.color", "output", "color"],
|
||||
["input.depth", "input", "depth"],
|
||||
["output.depth", "output", "depth"],
|
||||
]) {
|
||||
assert.equal(nodeDefinitions.gltf_standard.sockets[identity].direction, direction);
|
||||
assert.equal(nodeDefinitions.gltf_standard.sockets[identity].title, semanticName);
|
||||
}
|
||||
});
|
||||
|
||||
test("current culling fixture uses type-bit predicates and final socket versions", () => {
|
||||
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.cull.inputs.localAabb, [{ node: "mesh", socket: "localAabb" }]);
|
||||
assert.deepEqual(byId.type_words.inputs.value, [{ node: "mesh", socket: "type" }]);
|
||||
assert.equal(byId.ground.executor.version, 1);
|
||||
assert.equal(byId.ground.executor.version, 2);
|
||||
assert.equal(byId.ground.inputs.predicate[0].node, "ground_class");
|
||||
});
|
||||
|
||||
test("compiler texture sockets expose policy metadata without literal widgets", () => {
|
||||
for (const socket of ["colorTarget", "depthTarget"]) {
|
||||
for (const socket of ["input.color", "input.depth"]) {
|
||||
assert.equal(semanticCatalog.gltf_standard.inputs[socket].defaultPolicy, "compiler_texture");
|
||||
assert.equal(nodeDefinitions.gltf_standard.sockets[socket].default, undefined);
|
||||
}
|
||||
@@ -118,3 +134,56 @@ test("adapter preserves ordered multisocket links and indexed diagnostics", () =
|
||||
});
|
||||
assert.equal(diagnostic.source.linkId, "link_b");
|
||||
});
|
||||
|
||||
test("adapter lowers direction-qualified raster sockets and maps diagnostics", () => {
|
||||
const link = (id, fromNodeId, fromKey, toNodeId, toKey) => ({
|
||||
id,
|
||||
fromNodeId,
|
||||
fromSocketId: `${fromNodeId}:${fromKey}`,
|
||||
toNodeId,
|
||||
toSocketId: `${toNodeId}:${toKey}`,
|
||||
muted: false,
|
||||
extensions: {},
|
||||
});
|
||||
const raw = {
|
||||
graphId: GRAPH_ID,
|
||||
catalogVersion: CATALOG_VERSION,
|
||||
nodes: [
|
||||
authoredNode("texture", "texture"),
|
||||
authoredNode("mesh", "mesh"),
|
||||
authoredNode("raster", "gltf_standard"),
|
||||
authoredNode("target", "frame_out"),
|
||||
],
|
||||
links: [
|
||||
link("mesh_link", "mesh", "mesh", "raster", "mesh"),
|
||||
link("target_link", "texture", "texture", "raster", "input.color"),
|
||||
link("frame_link", "raster", "output.color", "target", "color"),
|
||||
],
|
||||
metadata: {},
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const graph = adaptFxNodeSnapshot(raw, 2);
|
||||
const rasterIndex = graph.nodes.findIndex((node) => node.id === "raster");
|
||||
const targetIndex = graph.nodes.findIndex((node) => node.id === "target");
|
||||
assert.deepEqual(graph.nodes[rasterIndex].inputs.color, [
|
||||
{ node: "texture", socket: "texture" },
|
||||
]);
|
||||
assert.deepEqual(graph.nodes[targetIndex].inputs.color, [
|
||||
{ node: "raster", socket: "color" },
|
||||
]);
|
||||
assert.equal(
|
||||
mapAuthoringDiagnostic(graph, {
|
||||
code: "GRAPH_INPUT_CARDINALITY",
|
||||
details: { path: `nodes[${rasterIndex}].inputs.depth` },
|
||||
}).source.socketId,
|
||||
"raster:input.depth",
|
||||
);
|
||||
assert.equal(
|
||||
mapAuthoringDiagnostic(graph, {
|
||||
code: "GRAPH_UNKNOWN_SOCKET",
|
||||
details: { path: `nodes[${targetIndex}].inputs.color[0].socket` },
|
||||
}).source.socketId,
|
||||
"raster:output.color",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { descriptors } from "../static/render-graph/catalog.js";
|
||||
test("all presets use current schemas, versions, and one frame output", () => {
|
||||
assert.equal(Object.keys(presets.renderGraphPresets).length, 13);
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [3, 3], name);
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [3, 4], name);
|
||||
assert.equal(new Set(graph.nodes.map((node) => node.id)).size, graph.nodes.length, name);
|
||||
assert.equal(graph.nodes.filter((node) => node.executor.key === "frame_out").length, 1, name);
|
||||
for (const node of graph.nodes)
|
||||
@@ -30,7 +30,7 @@ test("presets classify demo-owned enable and material bits through type.words[0]
|
||||
assert.deepEqual(byId.pbr_double.inputs.predicate, [{ node: "double_class", socket: "value" }], name);
|
||||
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
||||
assert.deepEqual(pipeline.inputs.mesh, [{ node: "mesh", socket: "mesh" }]);
|
||||
assert.equal(pipeline.executor.version, 1);
|
||||
assert.equal(pipeline.executor.version, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -48,11 +48,11 @@ test("culling adds a local-AABB expression to each material predicate", () => {
|
||||
test("scene pipelines directly share matching explicit color and depth targets", () => {
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
||||
const color = byId.ground.inputs.colorTarget;
|
||||
const depth = byId.ground.inputs.depthTarget;
|
||||
const color = byId.ground.inputs.color;
|
||||
const depth = byId.ground.inputs.depth;
|
||||
for (const id of ["ground", "pbr", "pbr_double"]) {
|
||||
assert.deepEqual(byId[id].inputs.colorTarget, color, `${name}:${id}`);
|
||||
assert.deepEqual(byId[id].inputs.depthTarget, depth, `${name}:${id}`);
|
||||
assert.deepEqual(byId[id].inputs.color, color, `${name}:${id}`);
|
||||
assert.deepEqual(byId[id].inputs.depth, depth, `${name}:${id}`);
|
||||
assert.equal(["ground", "pbr", "pbr_double"].includes(color[0].node), false, name);
|
||||
}
|
||||
const colorDescriptor = byId[color[0].node].parameters.texture;
|
||||
|
||||
Reference in New Issue
Block a user