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:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user