feat: add variadic render graph logic nodes

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-29 10:03:06 +00:00
co-authored by heaust
parent 0e866596c0
commit 90406ae18f
34 changed files with 1031 additions and 273 deletions
+187 -119
View File
@@ -306,10 +306,10 @@ pub fn parse_and_compile(bytes: &[u8]) -> Result<CompiledGraph, GraphError> {
.map_err(|_| GraphError::new("GRAPH_ENCODING_INVALID", "graph payload is not UTF-8"))?;
let probe: serde_json::Value = serde_json::from_str(text)
.map_err(|e| GraphError::new("GRAPH_JSON_INVALID", e.to_string()))?;
if probe.get("schemaVersion").and_then(|v| v.as_u64()) != Some(2) {
if probe.get("schemaVersion").and_then(|v| v.as_u64()) != Some(3) {
return Err(GraphError::new(
"GRAPH_SCHEMA_UNSUPPORTED",
"schemaVersion must be 2",
"schemaVersion must be 3",
));
}
let graph = serde_json::from_str(text)
@@ -790,15 +790,20 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
base.clone(),
)
})?;
if object.len() != contract.inputs.len() {
let default_inputs: Vec<_> = contract
.inputs
.iter()
.filter(|input| input.cardinality.max == 1)
.collect();
if object.len() != default_inputs.len() {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
"expression defaults must exactly match inputs",
base,
));
}
let mut defaults = Vec::with_capacity(contract.inputs.len());
for input in contract.inputs {
let mut defaults = Vec::with_capacity(default_inputs.len());
for input in default_inputs {
let key = format!("{}Default", input.name);
let value = object.get(&key).ok_or_else(|| {
error(
@@ -841,7 +846,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
let mut input_count = 0usize;
for (i, node) in graph.nodes.iter().enumerate() {
input_count = input_count.saturating_add(node.inputs.len());
input_count = input_count.saturating_add(node.inputs.values().map(Vec::len).sum::<usize>());
if input_count > 8192 {
return Err(error(
"GRAPH_LIMIT_EXCEEDED",
@@ -850,10 +855,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
));
}
}
if graph.schema_version != 2 {
if graph.schema_version != 3 {
return Err(GraphError::new(
"GRAPH_SCHEMA_UNSUPPORTED",
"schemaVersion must be 2",
"schemaVersion must be 3",
));
}
validate_name_length(&graph.graph_id, "graphId")?;
@@ -864,13 +869,14 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
] {
validate_name_length(value, path)?;
}
for (socket, r) in &n.inputs {
for (value, path) in [
(socket, format!("nodes[{i}].inputs.{socket}")),
(&r.node, format!("nodes[{i}].inputs.{socket}.node")),
(&r.socket, format!("nodes[{i}].inputs.{socket}.socket")),
] {
validate_name_length(value, path)?;
for (socket, refs) in &n.inputs {
validate_name_length(socket, format!("nodes[{i}].inputs.{socket}"))?;
for (index, r) in refs.iter().enumerate() {
validate_name_length(&r.node, format!("nodes[{i}].inputs.{socket}[{index}].node"))?;
validate_name_length(
&r.socket,
format!("nodes[{i}].inputs.{socket}[{index}].socket"),
)?;
}
}
}
@@ -891,24 +897,30 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
format!("nodes[{i}].id"),
));
}
for (socket, r) in &n.inputs {
for (value, path) in [
(socket, format!("nodes[{i}].inputs.{socket}")),
(&r.node, format!("nodes[{i}].inputs.{socket}.node")),
(&r.socket, format!("nodes[{i}].inputs.{socket}.socket")),
] {
validate_name_grammar(value, path)?;
for (socket, refs) in &n.inputs {
validate_name_grammar(socket, format!("nodes[{i}].inputs.{socket}"))?;
for (index, r) in refs.iter().enumerate() {
validate_name_grammar(
&r.node,
format!("nodes[{i}].inputs.{socket}[{index}].node"),
)?;
validate_name_grammar(
&r.socket,
format!("nodes[{i}].inputs.{socket}[{index}].socket"),
)?;
}
}
}
for (i, n) in graph.nodes.iter().enumerate() {
for (s, r) in &n.inputs {
if !ids.contains_key(r.node.as_str()) {
return Err(error(
"GRAPH_UNKNOWN_NODE",
"unknown input node",
format!("nodes[{i}].inputs.{s}.node"),
));
for (s, refs) in &n.inputs {
for (index, r) in refs.iter().enumerate() {
if !ids.contains_key(r.node.as_str()) {
return Err(error(
"GRAPH_UNKNOWN_NODE",
"unknown input node",
format!("nodes[{i}].inputs.{s}[{index}].node"),
));
}
}
}
}
@@ -978,27 +990,36 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
}
for (i, n) in graph.nodes.iter().enumerate() {
for (name, r) in &n.inputs {
let pn = ids[r.node.as_str()];
if !contracts[pn].outputs.iter().any(|out| out.name == r.socket) {
return Err(error(
"GRAPH_UNKNOWN_SOCKET",
"unknown output socket",
format!("nodes[{i}].inputs.{name}.socket"),
));
for (name, refs) in &n.inputs {
for (index, r) in refs.iter().enumerate() {
let pn = ids[r.node.as_str()];
if !contracts[pn].outputs.iter().any(|out| out.name == r.socket) {
return Err(error(
"GRAPH_UNKNOWN_SOCKET",
"unknown output socket",
format!("nodes[{i}].inputs.{name}[{index}].socket"),
));
}
}
}
}
for (i, n) in graph.nodes.iter().enumerate() {
for input in contracts[i].inputs {
if !n.inputs.contains_key(input.name) {
if input.cardinality == InputCardinality::RequiredOne {
return Err(error(
"GRAPH_SOCKET_CARDINALITY",
"required input is missing",
format!("nodes[{i}].inputs.{}", input.name),
));
}
let sources = n.inputs.get(input.name);
if sources.is_some_and(Vec::is_empty) {
return Err(error(
"GRAPH_SOCKET_CARDINALITY",
"present input bindings must not be empty",
format!("nodes[{i}].inputs.{}", input.name),
));
}
let count = sources.map_or(0, Vec::len);
if count < input.cardinality.min as usize || count > input.cardinality.max as usize {
return Err(error(
"GRAPH_SOCKET_CARDINALITY",
"input binding count is outside the accepted range",
format!("nodes[{i}].inputs.{}", input.name),
));
}
}
}
@@ -1006,35 +1027,59 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let mut bound: Vec<BTreeMap<&str, BoundInput>> = vec![BTreeMap::new(); graph.nodes.len()];
for (i, n) in graph.nodes.iter().enumerate() {
for input in contracts[i].inputs {
let Some(r) = n.inputs.get(input.name) else {
let Some(refs) = n.inputs.get(input.name) else {
continue;
};
let pn = ids[r.node.as_str()];
let (ordinal, out) = contracts[pn]
.outputs
.iter()
.enumerate()
.find(|(_, o)| o.name == r.socket)
.expect("producer sockets were globally validated");
if !accepts(input.accepted, out.semantic_type) {
return Err(error(
"GRAPH_SOCKET_TYPE_MISMATCH",
"socket type mismatch",
format!("nodes[{i}].inputs.{}", input.name),
));
for (source_index, r) in refs.iter().enumerate() {
let pn = ids[r.node.as_str()];
let (ordinal, out) = contracts[pn]
.outputs
.iter()
.enumerate()
.find(|(_, o)| o.name == r.socket)
.expect("producer sockets were globally validated");
if !accepts(input.accepted, out.semantic_type) {
return Err(error(
"GRAPH_SOCKET_TYPE_MISMATCH",
"socket type mismatch",
format!("nodes[{i}].inputs.{}[{source_index}]", input.name),
));
}
if source_index == 0 {
bound[i].insert(
input.name,
BoundInput {
producer: OutputKey(pn, ordinal as u16),
},
);
}
}
bound[i].insert(
input.name,
BoundInput {
producer: OutputKey(pn, ordinal as u16),
},
);
}
}
let mut edges = Vec::new();
for i in 0..graph.nodes.len() {
for (input_ordinal, input) in contracts[i].inputs.iter().enumerate() {
if let Some(b) = bound[i].get(input.name) {
for (source_index, b) in graph.nodes[i]
.inputs
.get(input.name)
.into_iter()
.flatten()
.enumerate()
.map(|(source_index, r)| {
let pn = ids[r.node.as_str()];
let ordinal = contracts[pn]
.outputs
.iter()
.position(|o| o.name == r.socket)
.unwrap();
(
source_index,
BoundInput {
producer: OutputKey(pn, ordinal as u16),
},
)
})
{
edges.push(DependencyEdge {
from_node: b.producer.0,
from_socket: contracts[b.producer.0].outputs[b.producer.1 as usize]
@@ -1044,7 +1089,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
to_node: i,
to_socket: input.name.into(),
consumer_input_ordinal: input_ordinal as u16,
resource: graph.nodes[i].inputs[input.name].clone(),
resource: graph.nodes[i].inputs[input.name][source_index].clone(),
});
}
}
@@ -2346,52 +2391,27 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let mut operands = Vec::new();
let mut operand_provenance = Vec::new();
for (ordinal, input) in contracts[i].inputs.iter().enumerate() {
if let Some(binding) = bound[i].get(input.name) {
let key = binding.producer;
let producer_type = contracts[key.0].outputs[key.1 as usize].semantic_type;
let operand_mesh = if contracts[key.0].key == "mesh" {
Some(output_ids[&OutputKey(key.0, 0)])
} else {
expression_provenance[&key]
};
let id = if producer_type.is_virtual() {
if contracts[key.0].key == "mesh" {
let mesh = output_ids[&OutputKey(key.0, 0)];
if mesh_root.is_some_and(|root| root != mesh) {
return Err(error(
"GRAPH_SOCKET_TYPE_MISMATCH",
"instance traversal has multiple mesh roots",
format!("nodes[{i}].inputs.{}", input.name),
));
}
mesh_root = Some(mesh);
let op = match producer_type {
SemanticType::U32x16 => ExpressionOp::InstanceType { mesh },
SemanticType::LocalAabb => ExpressionOp::LocalAabb { mesh },
_ => unreachable!(),
};
intern(
producer_type,
op,
graph.nodes[key.0]
.inputs
.get("")
.cloned()
.unwrap_or(NodeOutputRef {
node: graph.nodes[key.0].id.clone(),
socket: contracts[key.0].outputs[key.1 as usize].name.into(),
}),
Some(mesh),
)
} else {
expression_ids[&key]
let bindings: Vec<_> = graph.nodes[i]
.inputs
.get(input.name)
.into_iter()
.flatten()
.map(|r| {
let producer = ids[r.node.as_str()];
let output = contracts[producer]
.outputs
.iter()
.position(|o| o.name == r.socket)
.unwrap();
BoundInput {
producer: OutputKey(producer, output as u16),
}
} else {
})
.collect();
if bindings.is_empty() {
if input.cardinality.max > 1 {
continue;
};
operands.push(id);
operand_provenance.push(operand_mesh);
} else {
}
let literal = defaults[ordinal].clone();
operands.push(intern(
literal.semantic_type(),
@@ -2403,6 +2423,55 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
None,
));
operand_provenance.push(None);
} else {
for binding in bindings {
let key = binding.producer;
let producer_type = contracts[key.0].outputs[key.1 as usize].semantic_type;
let operand_mesh = if contracts[key.0].key == "mesh" {
Some(output_ids[&OutputKey(key.0, 0)])
} else {
expression_provenance[&key]
};
let id = if producer_type.is_virtual() {
if contracts[key.0].key == "mesh" {
let mesh = output_ids[&OutputKey(key.0, 0)];
if mesh_root.is_some_and(|root| root != mesh) {
return Err(error(
"GRAPH_SOCKET_TYPE_MISMATCH",
"instance traversal has multiple mesh roots",
format!("nodes[{i}].inputs.{}", input.name),
));
}
mesh_root = Some(mesh);
let op = match producer_type {
SemanticType::U32x16 => ExpressionOp::InstanceType { mesh },
SemanticType::LocalAabb => ExpressionOp::LocalAabb { mesh },
_ => unreachable!(),
};
intern(
producer_type,
op,
graph.nodes[key.0]
.inputs
.get("")
.and_then(|refs| refs.first().cloned())
.unwrap_or(NodeOutputRef {
node: graph.nodes[key.0].id.clone(),
socket: contracts[key.0].outputs[key.1 as usize]
.name
.into(),
}),
Some(mesh),
)
} else {
expression_ids[&key]
}
} else {
continue;
};
operands.push(id);
operand_provenance.push(operand_mesh);
}
}
}
let mut provenances = operand_provenance.into_iter().flatten();
@@ -2418,15 +2487,14 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let key = contracts[i].key;
let op = match key {
"not" => ExpressionOp::Not { value: operands[0] },
"and" | "or" | "xor" | "xnor" => ExpressionOp::BooleanBinary {
"and" | "or" | "xor" | "xnor" => ExpressionOp::Boolean {
operation: match key {
"and" => BooleanBinaryOp::And,
"or" => BooleanBinaryOp::Or,
"xor" => BooleanBinaryOp::Xor,
_ => BooleanBinaryOp::Xnor,
"and" => BooleanOp::And,
"or" => BooleanOp::Or,
"xor" => BooleanOp::Xor,
_ => BooleanOp::Xnor,
},
left: operands[0],
right: operands[1],
operands: operands.clone(),
},
"greater_than_f32" | "less_than_f32" | "equals_f32" => ExpressionOp::CompareF32 {
operation: if key.starts_with("greater") {
@@ -2636,7 +2704,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
));
}
Ok(CompiledGraph {
schema_version: 2,
schema_version: 3,
graph_id: graph.graph_id,
revision: graph.revision,
node_count: graph.nodes.len() as u32,
+47 -12
View File
@@ -39,9 +39,9 @@ pub enum FullscreenPolicy {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InputCardinality {
RequiredOne,
OptionalOne,
pub struct InputCardinality {
pub min: u8,
pub max: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
@@ -97,17 +97,19 @@ pub struct Contract {
}
use SemanticType::*;
const R: InputCardinality = InputCardinality::RequiredOne;
const O: InputCardinality = InputCardinality::OptionalOne;
const R: InputCardinality = InputCardinality { min: 1, max: 1 };
const O: InputCardinality = InputCardinality { min: 0, max: 1 };
const V: InputCardinality = InputCardinality { min: 0, max: 8 };
const fn i(
name: &'static str,
ty: SemanticType,
cardinality: InputCardinality,
role: InputRole,
) -> InputSocketContract {
let default_policy = match cardinality {
InputCardinality::RequiredOne => InputDefaultPolicy::None,
InputCardinality::OptionalOne => match role {
let default_policy = match (cardinality.min, cardinality.max) {
(_, 2..) => InputDefaultPolicy::None,
(1, _) => InputDefaultPolicy::None,
_ => match role {
InputRole::ColorTarget { .. } | InputRole::DepthTarget => {
InputDefaultPolicy::CompilerTexture
}
@@ -194,17 +196,50 @@ macro_rules! ex {
c!($k, 1, Expression, $ins, $outs, false, None)
};
}
const BOOL_VARIADIC_I: &[InputSocketContract] = &[i("inputs", Bool, V, InputRole::Expression)];
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!("pipeline", 4, Render, PIPE_I, PIPE_O, false, None),
ex!("and", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
ex!("or", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
c!(
"and",
2,
Expression,
BOOL_VARIADIC_I,
outs!("value":Bool),
false,
None
),
c!(
"or",
2,
Expression,
BOOL_VARIADIC_I,
outs!("value":Bool),
false,
None
),
ex!("not", ins!("operand":Bool), outs!("value":Bool)),
ex!("xor", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
ex!("xnor", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
c!(
"xor",
2,
Expression,
BOOL_VARIADIC_I,
outs!("value":Bool),
false,
None
),
c!(
"xnor",
2,
Expression,
BOOL_VARIADIC_I,
outs!("value":Bool),
false,
None
),
ex!(
"greater_than_f32",
ins!("left":F32,"right":F32),
+4 -5
View File
@@ -70,7 +70,7 @@ pub enum CompareOp {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BooleanBinaryOp {
pub enum BooleanOp {
And,
Or,
Xor,
@@ -93,10 +93,9 @@ pub enum ExpressionOp {
Not {
value: ExprId,
},
BooleanBinary {
operation: BooleanBinaryOp,
left: ExprId,
right: ExprId,
Boolean {
operation: BooleanOp,
operands: Vec<ExprId>,
},
CompareF32 {
operation: CompareOp,
+10 -8
View File
@@ -802,9 +802,9 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
));
}
}
if graph.schema_version != 2 {
if graph.schema_version != 3 {
return Err(invalid(
"compiled graph schema version must be 2",
"compiled graph schema version must be 3",
"schemaVersion",
));
}
@@ -1303,14 +1303,16 @@ fn validate_instance_traversal(graph: &CompiledGraph) -> Result<(), GraphError>
}
vec![*value]
}
ExpressionOp::BooleanBinary { left, right, .. } => {
ExpressionOp::Boolean { operands, .. } => {
if expression.semantic_type != SemanticType::Bool
|| ty(*left) != Some(SemanticType::Bool)
|| ty(*right) != Some(SemanticType::Bool)
|| operands.len() > 8
|| operands
.iter()
.any(|operand| ty(*operand) != Some(SemanticType::Bool))
{
return Err(invalid("boolean signature is invalid", &path));
}
vec![*left, *right]
operands.clone()
}
ExpressionOp::CompareF32 { left, right, .. } => {
if expression.semantic_type != SemanticType::Bool
@@ -1511,8 +1513,8 @@ fn expression_operands(op: &ExpressionOp) -> Vec<ExprId> {
| ExpressionOp::U32Bit { value, .. } => vec![*value],
ExpressionOp::AabbMin { aabb } | ExpressionOp::AabbMax { aabb } => vec![*aabb],
ExpressionOp::FrustumCulled { local_aabb, .. } => vec![*local_aabb],
ExpressionOp::BooleanBinary { left, right, .. }
| ExpressionOp::CompareF32 { left, right, .. }
ExpressionOp::Boolean { operands, .. } => operands.clone(),
ExpressionOp::CompareF32 { left, right, .. }
| ExpressionOp::CompareU32 { left, right, .. } => vec![*left, *right],
ExpressionOp::VectorConstruct { components } => components.clone(),
ExpressionOp::MatrixConstruct { columns } => columns.clone(),
+1 -1
View File
@@ -18,7 +18,7 @@ pub struct Node {
pub state: NodeState,
pub executor: ExecutorRef,
pub parameters: serde_json::Value,
pub inputs: BTreeMap<String, NodeOutputRef>,
pub inputs: BTreeMap<String, Vec<NodeOutputRef>>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
+36 -15
View File
@@ -7,7 +7,7 @@ fn compile_value(value: Value) -> Result<CompiledGraph, GraphError> {
}
fn input(node: &str, socket: &str) -> Value {
json!({ "node": node, "socket": socket })
json!([{ "node": node, "socket": socket }])
}
fn texture(id: &str, format: &str) -> Value {
@@ -38,7 +38,7 @@ fn node(id: &str, key: &str, version: u32, parameters: Value, inputs: Value) ->
}
pub(crate) fn full_cull_graph() -> Value {
json!({ "schemaVersion": 2, "graphId": "typed", "revision": 1, "nodes": [
json!({ "schemaVersion": 3, "graphId": "typed", "revision": 1, "nodes": [
texture("color", "rgba16_float"), texture("depth", "depth32_float"),
node("mesh", "mesh", 2, json!({}), json!({})),
node("words", "separate_u32x16", 1, json!({"valueDefault":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}),
@@ -47,8 +47,8 @@ pub(crate) fn full_cull_graph() -> Value {
node("cull", "frustum_cull", 2, json!({"camera":"active"}),
json!({"mesh":input("mesh","mesh"),"localAabb":input("mesh","localAabb")})),
node("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
node("class", "and", 1, json!({"leftDefault":true,"rightDefault":true}),
json!({"left":input("bits","bit0"),"right":input("visible","value")})),
node("class", "and", 2, json!({}),
json!({"inputs":[input("bits","bit0")[0].clone(),input("visible","value")[0].clone()]})),
node("pipeline", "pipeline", 4,
json!({"pipeline":"gltf_standard","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")})),
@@ -82,7 +82,7 @@ fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
.iter()
.find(|i| i.name == "predicate")
.unwrap();
assert_eq!(predicate.cardinality, InputCardinality::OptionalOne);
assert_eq!(predicate.cardinality, InputCardinality { min: 0, max: 1 });
for key in [
"and",
"xnor",
@@ -98,6 +98,23 @@ fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
}
}
#[test]
fn variadic_boolean_inputs_reject_empty_and_over_capacity_bindings() {
let mut empty = full_cull_graph();
empty["nodes"][7]["inputs"]["inputs"] = json!([]);
let error = compile_value(empty).unwrap_err();
assert_eq!(error.code, "GRAPH_SOCKET_CARDINALITY");
assert_eq!(error.details["path"], "nodes[7].inputs.inputs");
let mut over_capacity = full_cull_graph();
over_capacity["nodes"][7]["inputs"]["inputs"] = json!((0..9)
.map(|_| json!({ "node": "bits", "socket": "bit0" }))
.collect::<Vec<_>>());
let error = compile_value(over_capacity).unwrap_err();
assert_eq!(error.code, "GRAPH_SOCKET_CARDINALITY");
assert_eq!(error.details["path"], "nodes[7].inputs.inputs");
}
#[test]
fn typed_graph_builds_one_dense_deterministic_traversal() {
let first = compile_value(full_cull_graph()).unwrap();
@@ -356,7 +373,7 @@ fn expression_provenance_rejects_cross_mesh_values() {
}
fn implicit_pipeline_graph() -> Value {
json!({ "schemaVersion": 2, "graphId": "implicit", "revision": 1, "nodes": [
json!({ "schemaVersion": 3, "graphId": "implicit", "revision": 1, "nodes": [
node("mesh", "mesh", 2, json!({}), json!({})),
node("first", "pipeline", 4,
json!({"pipeline":"ground_plane","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
@@ -392,15 +409,19 @@ fn contract_v4_declares_strict_default_policies() {
.flat_map(|c| c.inputs)
.all(|input| matches!(
(input.cardinality, input.default_policy),
(InputCardinality::RequiredOne, InputDefaultPolicy::None)
| (
InputCardinality::OptionalOne,
InputDefaultPolicy::ParameterLiteral
)
| (
InputCardinality::OptionalOne,
InputDefaultPolicy::CompilerTexture
)
(
InputCardinality { min: 1, max: 1 },
InputDefaultPolicy::None
) | (
InputCardinality { min: 0, max: 1 },
InputDefaultPolicy::ParameterLiteral
) | (
InputCardinality { min: 0, max: 1 },
InputDefaultPolicy::CompilerTexture
) | (
InputCardinality { min: 0, max: 8 },
InputDefaultPolicy::None
)
)));
}
+72 -15
View File
@@ -1,7 +1,7 @@
//! Graph-owned instance predicate compute support.
use crate::render_graph::{
BooleanBinaryOp, CompareOp, ExpressionOp, InstanceTraversalPlan, SemanticType, TypedLiteral,
BooleanOp, CompareOp, ExpressionOp, InstanceTraversalPlan, SemanticType, TypedLiteral,
};
use super::gpu_scene::{DrawIndexedIndirect, GpuSceneCache};
@@ -58,21 +58,29 @@ pub fn generate_wgsl(plan: &InstanceTraversalPlan) -> Result<String, String> {
ExpressionOp::InstanceType { .. } => "types[i]".into(),
ExpressionOp::LocalAabb { .. } => "LocalAabb(aabbs[i].min.xyz,aabbs[i].max.xyz)".into(),
ExpressionOp::Not { value } => format!("!{}", x(*value)),
ExpressionOp::BooleanBinary {
ExpressionOp::Boolean {
operation,
left,
right,
} => format!(
"({} {} {})",
x(*left),
match operation {
BooleanBinaryOp::And => "&&",
BooleanBinaryOp::Or => "||",
BooleanBinaryOp::Xor => "!=",
BooleanBinaryOp::Xnor => "==",
},
x(*right)
),
operands,
} => {
let identity = matches!(operation, BooleanOp::And | BooleanOp::Xnor);
let operator = match operation {
BooleanOp::And => "&&",
BooleanOp::Or => "||",
BooleanOp::Xor | BooleanOp::Xnor => "!=",
};
let folded = operands
.iter()
.map(|operand| x(*operand))
.reduce(|left, right| format!("({left} {operator} {right})"))
.unwrap_or_else(|| identity.to_string());
if matches!(operation, BooleanOp::Xnor) && operands.len() > 1 {
format!("!{folded}")
} else if matches!(operation, BooleanOp::Xnor) && !operands.is_empty() {
format!("!({folded})")
} else {
folded
}
}
ExpressionOp::CompareF32 {
operation,
left,
@@ -432,6 +440,55 @@ mod tests {
assert_eq!(wgsl_type(&SemanticType::Mat4).unwrap(), "mat4x4<f32>");
}
#[test]
fn variadic_boolean_wgsl_uses_identities_and_ordered_parity() {
let expressions = vec![
expression(
SemanticType::Bool,
ExpressionOp::Boolean {
operation: BooleanOp::And,
operands: vec![],
},
),
expression(
SemanticType::Bool,
ExpressionOp::Boolean {
operation: BooleanOp::Xor,
operands: vec![],
},
),
expression(
SemanticType::Bool,
ExpressionOp::Boolean {
operation: BooleanOp::Xnor,
operands: vec![crate::render_graph::ExprId(0)],
},
),
expression(
SemanticType::Bool,
ExpressionOp::Boolean {
operation: BooleanOp::Xnor,
operands: vec![
crate::render_graph::ExprId(0),
crate::render_graph::ExprId(1),
crate::render_graph::ExprId(2),
],
},
),
];
let wgsl = generate_wgsl(&InstanceTraversalPlan {
mesh: 0,
expressions: ExpressionPlan { expressions },
pipelines: vec![],
requires_camera: false,
})
.unwrap();
assert!(wgsl.contains("let e0=true;"));
assert!(wgsl.contains("let e1=false;"));
assert!(wgsl.contains("let e2=!(e0);"));
assert!(wgsl.contains("let e3=!((e0 != e1) != e2);"));
}
#[test]
fn u32_construct_wgsl_is_parenthesized() {
let plan = InstanceTraversalPlan {
+1 -1
View File
@@ -1155,7 +1155,7 @@ mod switch_request_tests {
assert_eq!(pending, None);
let invalid_replacement =
br#"{"schemaVersion":2,"graphId":"switch","revision":2,"nodes":[],"unexpected":true}"#;
br#"{"schemaVersion":3,"graphId":"switch","revision":2,"nodes":[],"unexpected":true}"#;
assert_eq!(
registry.compile(invalid_replacement).unwrap_err().code,
"GRAPH_JSON_INVALID"