diff --git a/renderer/src/render_graph/compiler.rs b/renderer/src/render_graph/compiler.rs index f12a72f..4a690df 100644 --- a/renderer/src/render_graph/compiler.rs +++ b/renderer/src/render_graph/compiler.rs @@ -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::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 { } } - // 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::>::new(); + // Preserve authored readers that must finish before an attachment version is overwritten. let mut ordinary_consumers = HashMap::>::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 { )); } 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 { 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 { 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 { // 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 { 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 { } 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 { 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 { 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 { 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 { } // 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() diff --git a/renderer/src/render_graph/contracts.rs b/renderer/src/render_graph/contracts.rs index 59d28d5..af46d1f 100644 --- a/renderer/src/render_graph/contracts.rs +++ b/renderer/src/render_graph/contracts.rs @@ -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, diff --git a/renderer/src/render_graph/plan.rs b/renderer/src/render_graph/plan.rs index 03749b6..58a5ac8 100644 --- a/renderer/src/render_graph/plan.rs +++ b/renderer/src/render_graph/plan.rs @@ -221,7 +221,6 @@ pub enum NormalizedParameters { defaults: Vec, }, Raster { - draw_order: i32, depth_compare: CompareFunction, depth_write_enabled: bool, clear_depth: f32, diff --git a/renderer/src/render_graph/runtime.rs b/renderer/src/render_graph/runtime.rs index 98f8bdf..7938909 100644 --- a/renderer/src/render_graph/runtime.rs +++ b/renderer/src/render_graph/runtime.rs @@ -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 = 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", diff --git a/renderer/src/render_graph/tests.rs b/renderer/src/render_graph/tests.rs index 669c6b3..3082ebd 100644 --- a/renderer/src/render_graph/tests.rs +++ b/renderer/src/render_graph/tests.rs @@ -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::>(), - ["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::>(), + [ + "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; diff --git a/renderer/src/renderer/mod.rs b/renderer/src/renderer/mod.rs index 0cf47be..608c9fb 100644 --- a/renderer/src/renderer/mod.rs +++ b/renderer/src/renderer/mod.rs @@ -2091,7 +2091,6 @@ impl Renderer { }) .transpose()?; let NormalizedParameters::Raster { - draw_order: _, depth_compare, depth_write_enabled, .. diff --git a/static/render-graph/adapter.js b/static/render-graph/adapter.js index 01c2a8a..8a263ce 100644 --- a/static/render-graph/adapter.js +++ b/static/render-graph/adapter.js @@ -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, diff --git a/static/render-graph/catalog.js b/static/render-graph/catalog.js index 49afc3b..ad31bfa 100644 --- a/static/render-graph/catalog.js +++ b/static/render-graph/catalog.js @@ -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"; } diff --git a/static/render-graph/presets.js b/static/render-graph/presets.js index 315e194..89610e1 100644 --- a/static/render-graph/presets.js +++ b/static/render-graph/presets.js @@ -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), diff --git a/tests/fxnode-composition.test.js b/tests/fxnode-composition.test.js index 262f349..19df477 100644 --- a/tests/fxnode-composition.test.js +++ b/tests/fxnode-composition.test.js @@ -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( diff --git a/tests/render-graph-authoring.test.js b/tests/render-graph-authoring.test.js index fe526ed..e53cb03 100644 --- a/tests/render-graph-authoring.test.js +++ b/tests/render-graph-authoring.test.js @@ -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", + ); +}); diff --git a/tests/render-graph-presets.test.js b/tests/render-graph-presets.test.js index 46e763b..ecdd515 100644 --- a/tests/render-graph-presets.test.js +++ b/tests/render-graph-presets.test.js @@ -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;