feat: compile pipeline-specific render pass cohorts
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:
@@ -21,8 +21,8 @@ struct CullParameters {
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct PipelineParameters {
|
||||
pipeline: String,
|
||||
struct RasterParameters {
|
||||
draw_order: i32,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
@@ -137,6 +137,11 @@ fn color(value: [f32; 4], min: f32, max: f32, base: &str) -> Result<[f32; 3], Gr
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct OutputKey(usize, u16);
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum AttachmentRoot {
|
||||
Authored(OutputKey),
|
||||
CompilerDefault { node: usize, ordinal: u16 },
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum TransitionTargetKey {
|
||||
Authored(OutputKey),
|
||||
CompilerDefaultInput {
|
||||
@@ -244,6 +249,7 @@ fn reaches(
|
||||
from: usize,
|
||||
to: usize,
|
||||
outgoing_edges: &[Vec<usize>],
|
||||
ordering_outgoing: &[Vec<usize>],
|
||||
edges: &[DependencyEdge],
|
||||
live: &HashSet<usize>,
|
||||
memo: &mut HashMap<(usize, usize), bool>,
|
||||
@@ -268,6 +274,11 @@ fn reaches(
|
||||
stack.push(next);
|
||||
}
|
||||
}
|
||||
for &next in &ordering_outgoing[node] {
|
||||
if live.contains(&next) {
|
||||
stack.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
memo.insert((from, to), answer);
|
||||
answer
|
||||
@@ -741,21 +752,9 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
descriptor: normalize_texture(p.texture, &base)?,
|
||||
}
|
||||
}
|
||||
"pipeline" => {
|
||||
let p: PipelineParameters =
|
||||
key if contract(key).is_some_and(|contract| contract.is_raster_draw()) => {
|
||||
let p: RasterParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
let valid_name = !p.pipeline.is_empty()
|
||||
&& p.pipeline.len() <= 64
|
||||
&& p.pipeline.bytes().enumerate().all(|(i, c)| {
|
||||
c == b'_' || c.is_ascii_alphanumeric() && (i > 0 || c.is_ascii_alphabetic())
|
||||
});
|
||||
if !valid_name {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"pipeline must be a 1-64 byte identifier",
|
||||
format!("{base}.pipeline"),
|
||||
));
|
||||
}
|
||||
if !p.clear_depth.is_finite() || !(0.0..=1.0).contains(&p.clear_depth) {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
@@ -770,8 +769,8 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
format!("{base}.clearColor"),
|
||||
));
|
||||
}
|
||||
NormalizedParameters::Pipeline {
|
||||
pipeline: p.pipeline,
|
||||
NormalizedParameters::Raster {
|
||||
draw_order: p.draw_order,
|
||||
depth_compare: p.depth_compare,
|
||||
depth_write_enabled: p.depth_write_enabled,
|
||||
clear_depth: p.clear_depth,
|
||||
@@ -1102,6 +1101,271 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
e.producer_output_ordinal,
|
||||
)
|
||||
});
|
||||
let is_normalizable_target = |edge: &DependencyEdge| {
|
||||
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
|
||||
)
|
||||
};
|
||||
let is_exact_reader = |edge: &DependencyEdge| {
|
||||
let input = &contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize];
|
||||
input.role == InputRole::SampledTexture
|
||||
|| (input.role == InputRole::SemanticRead
|
||||
&& contracts[edge.from_node].outputs[edge.producer_output_ordinal as usize]
|
||||
.semantic_type
|
||||
== SemanticType::Texture)
|
||||
};
|
||||
// Compute demand from authored resource dependencies only. In particular,
|
||||
// a later WAR ordering edge must never resurrect its reader.
|
||||
let mut provisional_live = HashSet::new();
|
||||
let mut provisional_stack: Vec<_> = contracts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, contract)| {
|
||||
contract.inherently_observable && graph.nodes[*i].state == NodeState::Enabled
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
let mut authored_deps = vec![Vec::new(); graph.nodes.len()];
|
||||
for edge in &edges {
|
||||
authored_deps[edge.to_node].push(edge.from_node);
|
||||
}
|
||||
while let Some(node) = provisional_stack.pop() {
|
||||
if provisional_live.insert(node) {
|
||||
provisional_stack.extend(authored_deps[node].iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve authored, non-normalized dataflow before attachment normalization. It
|
||||
// is used both to diagnose impossible draw-order normalization and to find
|
||||
// readers which must finish before an attachment version is overwritten.
|
||||
let ordinary_edges: Vec<_> = edges
|
||||
.iter()
|
||||
.filter(|edge| !is_normalizable_target(edge))
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut target_consumers = HashMap::<OutputKey, Vec<usize>>::new();
|
||||
let mut ordinary_consumers = HashMap::<OutputKey, Vec<usize>>::new();
|
||||
for edge in &edges {
|
||||
let key = OutputKey(edge.from_node, edge.producer_output_ordinal);
|
||||
if is_normalizable_target(edge) {
|
||||
target_consumers.entry(key).or_default().push(edge.to_node);
|
||||
} else if is_exact_reader(edge) && provisional_live.contains(&edge.to_node) {
|
||||
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()
|
||||
.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
|
||||
// one texture. Turn both forms into an explicit, deterministic SSA chain.
|
||||
fn attachment_root(
|
||||
node: usize,
|
||||
ordinal: u16,
|
||||
bound: &[BTreeMap<&str, BoundInput>],
|
||||
contracts: &[&Contract],
|
||||
colors: &mut HashMap<(usize, u16), u8>,
|
||||
roots: &mut HashMap<(usize, u16), AttachmentRoot>,
|
||||
) -> Result<AttachmentRoot, GraphError> {
|
||||
if let Some(&root) = roots.get(&(node, ordinal)) {
|
||||
return Ok(root);
|
||||
}
|
||||
if colors.get(&(node, ordinal)) == Some(&1) {
|
||||
return Err(error(
|
||||
"GRAPH_ATTACHMENT_LINEAGE_INVALID",
|
||||
"attachment lineage is cyclic",
|
||||
format!("nodes[{node}].inputs"),
|
||||
));
|
||||
}
|
||||
colors.insert((node, ordinal), 1);
|
||||
let socket = if ordinal == 0 {
|
||||
"colorTarget"
|
||||
} else {
|
||||
"depthTarget"
|
||||
};
|
||||
let root = match bound[node].get(socket).map(|binding| binding.producer) {
|
||||
None => AttachmentRoot::CompilerDefault { node, ordinal },
|
||||
Some(output)
|
||||
if !contracts[output.0].is_raster_draw()
|
||||
&& contracts[output.0].outputs[output.1 as usize].semantic_type
|
||||
== SemanticType::Texture =>
|
||||
{
|
||||
AttachmentRoot::Authored(output)
|
||||
}
|
||||
Some(output) if contracts[output.0].is_raster_draw() && output.1 == ordinal => {
|
||||
attachment_root(output.0, ordinal, bound, contracts, colors, roots)?
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(error(
|
||||
"GRAPH_ATTACHMENT_LINEAGE_INVALID",
|
||||
"attachment lineage has no texture or default root",
|
||||
format!("nodes[{node}].inputs.{socket}"),
|
||||
))
|
||||
}
|
||||
};
|
||||
colors.insert((node, ordinal), 2);
|
||||
roots.insert((node, ordinal), root);
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
let raster_nodes: Vec<_> = contracts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, contract)| contract.is_raster_draw().then_some(i))
|
||||
.collect();
|
||||
let mut aliases = HashMap::<OutputKey, OutputKey>::new();
|
||||
for ordinal in 0..=1u16 {
|
||||
let mut colors = HashMap::new();
|
||||
let mut roots = HashMap::new();
|
||||
let mut cohorts = BTreeMap::<AttachmentRoot, Vec<usize>>::new();
|
||||
for &node in &raster_nodes {
|
||||
let root = attachment_root(node, ordinal, &bound, &contracts, &mut colors, &mut roots)?;
|
||||
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)
|
||||
});
|
||||
// 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));
|
||||
if at_cut || position + 1 == members.len() {
|
||||
let terminal = OutputKey(members[position], ordinal);
|
||||
for &member in &members[segment_start..=position] {
|
||||
aliases.insert(OutputKey(member, ordinal), terminal);
|
||||
}
|
||||
if let Some(&next_writer) = members.get(position + 1) {
|
||||
for &member in &members[segment_start..=position] {
|
||||
let output = OutputKey(member, ordinal);
|
||||
for &reader in ordinary_consumers.get(&output).into_iter().flatten() {
|
||||
if reader != next_writer {
|
||||
ordering_edges.push((reader, next_writer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
segment_start = position + 1;
|
||||
}
|
||||
}
|
||||
let socket = if ordinal == 0 {
|
||||
"colorTarget"
|
||||
} else {
|
||||
"depthTarget"
|
||||
};
|
||||
for (position, &member) in members.iter().enumerate() {
|
||||
let target = if position == 0 {
|
||||
match root {
|
||||
AttachmentRoot::Authored(output) => Some(output),
|
||||
AttachmentRoot::CompilerDefault { .. } => None,
|
||||
}
|
||||
} else {
|
||||
Some(OutputKey(members[position - 1], ordinal))
|
||||
};
|
||||
bound[member].remove(socket);
|
||||
if let Some(producer) = target {
|
||||
bound[member].insert(socket, BoundInput { producer });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ordering_edges.sort_unstable();
|
||||
ordering_edges.dedup();
|
||||
// Replace authored attachment dependencies with the canonical WAW chain,
|
||||
// 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"))
|
||||
});
|
||||
for &node in &raster_nodes {
|
||||
for (socket, ordinal) in [("colorTarget", 0u16), ("depthTarget", 1u16)] {
|
||||
let Some(binding) = bound[node].get(socket) else {
|
||||
continue;
|
||||
};
|
||||
let input_ordinal = contracts[node]
|
||||
.inputs
|
||||
.iter()
|
||||
.position(|input| input.name == socket)
|
||||
.expect("raster target contract") as u16;
|
||||
edges.push(DependencyEdge {
|
||||
from_node: binding.producer.0,
|
||||
from_socket: contracts[binding.producer.0].outputs[binding.producer.1 as usize]
|
||||
.name
|
||||
.into(),
|
||||
producer_output_ordinal: binding.producer.1,
|
||||
to_node: node,
|
||||
to_socket: socket.into(),
|
||||
consumer_input_ordinal: input_ordinal,
|
||||
resource: NodeOutputRef {
|
||||
node: graph.nodes[binding.producer.0].id.clone(),
|
||||
socket: contracts[binding.producer.0].outputs[binding.producer.1 as usize]
|
||||
.name
|
||||
.into(),
|
||||
},
|
||||
});
|
||||
let _ = ordinal;
|
||||
}
|
||||
}
|
||||
for node in 0..graph.nodes.len() {
|
||||
for input in contracts[node].inputs.iter().filter(|input| {
|
||||
matches!(
|
||||
input.role,
|
||||
InputRole::SampledTexture | InputRole::SemanticRead
|
||||
)
|
||||
}) {
|
||||
let Some(binding) = bound[node].get_mut(input.name) else {
|
||||
continue;
|
||||
};
|
||||
if input.role == InputRole::SemanticRead
|
||||
&& contracts[binding.producer.0].outputs[binding.producer.1 as usize].semantic_type
|
||||
!= SemanticType::Texture
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(&terminal) = aliases.get(&binding.producer) {
|
||||
binding.producer = terminal;
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in &mut edges {
|
||||
if is_exact_reader(edge) {
|
||||
let key = OutputKey(edge.from_node, edge.producer_output_ordinal);
|
||||
if let Some(&terminal) = aliases.get(&key) {
|
||||
edge.from_node = terminal.0;
|
||||
edge.producer_output_ordinal = terminal.1;
|
||||
edge.from_socket = contracts[terminal.0].outputs[terminal.1 as usize]
|
||||
.name
|
||||
.into();
|
||||
edge.resource = NodeOutputRef {
|
||||
node: graph.nodes[terminal.0].id.clone(),
|
||||
socket: edge.from_socket.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
edges.sort_by_key(|e| {
|
||||
(
|
||||
e.to_node,
|
||||
e.consumer_input_ordinal,
|
||||
e.from_node,
|
||||
e.producer_output_ordinal,
|
||||
)
|
||||
});
|
||||
let mut deps = vec![Vec::new(); graph.nodes.len()];
|
||||
for edge in &edges {
|
||||
deps[edge.to_node].push(edge.from_node);
|
||||
@@ -1122,6 +1386,42 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
stack.extend(deps[i].iter().copied());
|
||||
}
|
||||
}
|
||||
// A canonical attachment writer edge may not reverse authored ordinary
|
||||
// dataflow. Keep this distinct from generic cycle reporting so authors get
|
||||
// the draw-order parameter which made normalization impossible.
|
||||
let mut ordinary_outgoing = vec![Vec::new(); graph.nodes.len()];
|
||||
for edge in &ordinary_edges {
|
||||
ordinary_outgoing[edge.from_node].push(edge.to_node);
|
||||
}
|
||||
let ordinary_reaches = |from: usize, to: usize| {
|
||||
let mut seen = vec![false; graph.nodes.len()];
|
||||
let mut pending = vec![from];
|
||||
while let Some(node) = pending.pop() {
|
||||
if node == to {
|
||||
return true;
|
||||
}
|
||||
if !seen[node] {
|
||||
seen[node] = true;
|
||||
pending.extend(ordinary_outgoing[node].iter().copied());
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
let mut draw_order_conflicts: Vec<_> = edges
|
||||
.iter()
|
||||
.filter(|edge| {
|
||||
contracts[edge.from_node].is_raster_draw()
|
||||
&& contracts[edge.to_node].is_raster_draw()
|
||||
&& matches!(
|
||||
contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize].role,
|
||||
InputRole::ColorTarget { .. } | InputRole::DepthTarget
|
||||
)
|
||||
&& ordinary_reaches(edge.to_node, edge.from_node)
|
||||
})
|
||||
.map(|edge| edge.to_node)
|
||||
.collect();
|
||||
draw_order_conflicts.sort_unstable();
|
||||
draw_order_conflicts.dedup();
|
||||
// IDs are independent of scheduling: original node order, then contract output order.
|
||||
// Source nodes expose only outputs that survived active-edge/liveness analysis;
|
||||
// executable nodes retain their complete output shape for runtime lowering.
|
||||
@@ -1215,10 +1515,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let mut default_roots: Vec<DefaultDraft> = Vec::new();
|
||||
let mut default_targets = HashMap::new();
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i) || contracts[i].key != "pipeline" {
|
||||
if !live.contains(&i) || !contracts[i].is_raster_draw() {
|
||||
continue;
|
||||
}
|
||||
for (input_ordinal, (socket, role, format, opposite)) in [
|
||||
for (socket, role, format, opposite) in [
|
||||
(
|
||||
"colorTarget",
|
||||
CompilerTextureRole::ColorTarget,
|
||||
@@ -1233,12 +1533,16 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if bound[i].contains_key(socket) {
|
||||
continue;
|
||||
}
|
||||
let input_ordinal = input_ordinal as u16 + 2;
|
||||
let input_ordinal = contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.position(|input| input.name == socket)
|
||||
.expect("compiler target has a contract input")
|
||||
as u16;
|
||||
let key = TransitionTargetKey::CompilerDefaultInput {
|
||||
owner_node: i,
|
||||
input_ordinal,
|
||||
@@ -1267,8 +1571,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
if !live.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
|
||||
"pipeline" => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||
let transition_sockets: &[(&str, u16)] = match contracts[i] {
|
||||
ref contract if contract.is_raster_draw() => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||
_ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)],
|
||||
_ => continue,
|
||||
};
|
||||
@@ -1388,6 +1692,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
outgoing_edges[edge.from_node].push(index);
|
||||
}
|
||||
}
|
||||
let mut ordering_outgoing = vec![Vec::new(); graph.nodes.len()];
|
||||
for &(from, to) in &ordering_edges {
|
||||
if live.contains(&from) && live.contains(&to) {
|
||||
ordering_outgoing[from].push(to);
|
||||
}
|
||||
}
|
||||
for outgoing in &mut outgoing_edges {
|
||||
outgoing.sort_by_key(|&index| {
|
||||
let edge = &edges[index];
|
||||
@@ -1404,7 +1714,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
if !live.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
if contracts[i].key == "pipeline" {
|
||||
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")
|
||||
@@ -1532,7 +1842,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket),
|
||||
));
|
||||
}
|
||||
if contracts[edge.from_node].key != "pipeline" || edge.producer_output_ordinal != 0 {
|
||||
if !contracts[edge.from_node].is_raster_draw() || edge.producer_output_ordinal != 0 {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"multisampled color texture is not a produced pipeline color",
|
||||
@@ -1571,6 +1881,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
i,
|
||||
next.writer_node,
|
||||
&outgoing_edges,
|
||||
&ordering_outgoing,
|
||||
&edges,
|
||||
&live,
|
||||
&mut reachability,
|
||||
@@ -1587,7 +1898,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
|
||||
// Validate every independently resolved attachment before graph cycle reporting.
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i) || contracts[i].key != "pipeline" {
|
||||
if !live.contains(&i) || !contracts[i].is_raster_draw() {
|
||||
continue;
|
||||
}
|
||||
let cd = version_of
|
||||
@@ -1780,11 +2091,22 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
|
||||
// Stable Kahn scheduling is deliberately after resource and access validation.
|
||||
if let Some(&node) = draw_order_conflicts.iter().find(|node| live.contains(node)) {
|
||||
return Err(error(
|
||||
"GRAPH_DRAW_ORDER_CONFLICT",
|
||||
"draw order conflicts with authored dependencies",
|
||||
format!("nodes[{node}].parameters.drawOrder"),
|
||||
));
|
||||
}
|
||||
let mut indegree = vec![0; graph.nodes.len()];
|
||||
for &node in &live {
|
||||
indegree[node] = edges
|
||||
.iter()
|
||||
.filter(|edge| edge.to_node == node && live.contains(&edge.from_node))
|
||||
.count()
|
||||
+ ordering_edges
|
||||
.iter()
|
||||
.filter(|(from, to)| *to == node && live.contains(from))
|
||||
.count();
|
||||
}
|
||||
let mut queue = BinaryHeap::new();
|
||||
@@ -1803,6 +2125,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
queue.push(Reverse(consumer));
|
||||
}
|
||||
}
|
||||
for &consumer in &ordering_outgoing[node] {
|
||||
indegree[consumer] -= 1;
|
||||
if indegree[consumer] == 0 {
|
||||
queue.push(Reverse(consumer));
|
||||
}
|
||||
}
|
||||
}
|
||||
if order.len() != live.len() {
|
||||
let residual: Vec<_> = (0..graph.nodes.len())
|
||||
@@ -1811,6 +2139,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
fn cycle_dfs(
|
||||
node: usize,
|
||||
outgoing_edges: &[Vec<usize>],
|
||||
ordering_outgoing: &[Vec<usize>],
|
||||
edges: &[DependencyEdge],
|
||||
residual: &[bool],
|
||||
colors: &mut [u8],
|
||||
@@ -1829,6 +2158,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
if let Some(cycle) = cycle_dfs(
|
||||
to,
|
||||
outgoing_edges,
|
||||
ordering_outgoing,
|
||||
edges,
|
||||
residual,
|
||||
colors,
|
||||
@@ -1848,6 +2178,31 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
return Some(cycle);
|
||||
}
|
||||
}
|
||||
for &to in &ordering_outgoing[node] {
|
||||
if !residual[to] {
|
||||
continue;
|
||||
}
|
||||
if colors[to] == 0 {
|
||||
if cycle_dfs(
|
||||
to,
|
||||
outgoing_edges,
|
||||
ordering_outgoing,
|
||||
edges,
|
||||
residual,
|
||||
colors,
|
||||
node_stack,
|
||||
edge_stack,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
// Ordering edges are synthetic and have no authored
|
||||
// socket payload, but still constitute a real cycle.
|
||||
return Some(Vec::new());
|
||||
}
|
||||
} else if colors[to] == 1 {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
}
|
||||
node_stack.pop();
|
||||
colors[node] = 2;
|
||||
None
|
||||
@@ -1859,6 +2214,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
cycle = cycle_dfs(
|
||||
node,
|
||||
&outgoing_edges,
|
||||
&ordering_outgoing,
|
||||
&edges,
|
||||
&residual,
|
||||
&mut colors,
|
||||
@@ -2189,15 +2545,15 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
.collect();
|
||||
let mut accesses = Vec::new();
|
||||
let kind = match contracts[i].key {
|
||||
"pipeline" => {
|
||||
_ if contracts[i].is_raster_draw() => {
|
||||
let color = output_ids[&OutputKey(i, 0)];
|
||||
let depth = output_ids[&OutputKey(i, 1)];
|
||||
let clear = match params[i] {
|
||||
NormalizedParameters::Pipeline { clear_color, .. } => clear_color,
|
||||
NormalizedParameters::Raster { clear_color, .. } => clear_color,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let clear_depth = match ¶ms[i] {
|
||||
NormalizedParameters::Pipeline { clear_depth, .. } => *clear_depth,
|
||||
NormalizedParameters::Raster { clear_depth, .. } => *clear_depth,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
|
||||
@@ -2280,20 +2636,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
},
|
||||
});
|
||||
}
|
||||
ExecutionKind::Render {
|
||||
color_attachments: vec![ColorAttachmentPlan {
|
||||
resource: color,
|
||||
resolve_target,
|
||||
location: 0,
|
||||
load: cl,
|
||||
store: source_store,
|
||||
}],
|
||||
depth_stencil: Some(DepthStencilAttachmentPlan {
|
||||
resource: depth,
|
||||
load: dl,
|
||||
store: StoreOp::Store,
|
||||
}),
|
||||
}
|
||||
ExecutionKind::RasterDraw
|
||||
}
|
||||
_ if contracts[i].fullscreen_policy.is_some() => {
|
||||
let color = output_ids[&OutputKey(i, 0)];
|
||||
@@ -2321,16 +2664,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
full_overwrite: true,
|
||||
},
|
||||
});
|
||||
ExecutionKind::Render {
|
||||
color_attachments: vec![ColorAttachmentPlan {
|
||||
resource: color,
|
||||
resolve_target: None,
|
||||
location: 0,
|
||||
load,
|
||||
store: StoreOp::Store,
|
||||
}],
|
||||
depth_stencil: None,
|
||||
}
|
||||
ExecutionKind::Fullscreen
|
||||
}
|
||||
"frame_out" => {
|
||||
let r = input_resource("color");
|
||||
@@ -2584,7 +2918,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let mut predicates = Vec::new();
|
||||
for (execution, compiled) in executions.iter().enumerate() {
|
||||
let node = compiled.original_node_index as usize;
|
||||
if contracts[node].key != "pipeline" {
|
||||
if !contracts[node].is_raster_draw() {
|
||||
continue;
|
||||
}
|
||||
let mesh = output_ids[&bound[node]["mesh"].producer];
|
||||
@@ -2599,7 +2933,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let predicate = if let Some(binding) = bound[node].get("predicate") {
|
||||
expression_ids[&binding.producer]
|
||||
} else {
|
||||
let NormalizedParameters::Pipeline {
|
||||
let NormalizedParameters::Raster {
|
||||
predicate_default, ..
|
||||
} = params[node]
|
||||
else {
|
||||
@@ -2651,9 +2985,15 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dense lifetimes touch bindings, outputs, and accesses.
|
||||
let render_passes = build_render_passes(&executions, &resources, &families);
|
||||
let execution_pass: Vec<u32> = render_passes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(pass, value)| value.executions.iter().map(move |_| pass as u32))
|
||||
.collect();
|
||||
// Dense lifetimes use physical pass ordinals; producer metadata remains logical.
|
||||
for (ordinal, e) in executions.iter().enumerate() {
|
||||
let ordinal = ordinal as u32;
|
||||
let ordinal = execution_pass[ordinal];
|
||||
let mut touched = BTreeSet::new();
|
||||
for x in &e.inputs {
|
||||
touched.insert(x.resource);
|
||||
@@ -2710,6 +3050,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
node_count: graph.nodes.len() as u32,
|
||||
resources,
|
||||
executions,
|
||||
render_passes,
|
||||
texture_families: families,
|
||||
allocation_classes: classes,
|
||||
culled_node_count: (graph.nodes.len() - live.len()) as u32,
|
||||
@@ -2719,6 +3060,170 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn execution_attachments(
|
||||
execution: &CompiledExecution,
|
||||
) -> (Vec<ColorAttachmentPlan>, Option<DepthStencilAttachmentPlan>) {
|
||||
let mut colors = Vec::new();
|
||||
let mut depth = None;
|
||||
for access in &execution.accesses {
|
||||
match access.mode {
|
||||
AccessMode::ColorAttachment {
|
||||
location,
|
||||
load,
|
||||
store,
|
||||
..
|
||||
} => colors.push(ColorAttachmentPlan {
|
||||
resource: access.resource,
|
||||
resolve_target: execution.accesses.iter().find_map(|candidate| {
|
||||
match candidate.mode {
|
||||
AccessMode::ColorResolve {
|
||||
source,
|
||||
location: l,
|
||||
} if source == access.resource && l == location => Some(candidate.resource),
|
||||
_ => None,
|
||||
}
|
||||
}),
|
||||
location,
|
||||
load,
|
||||
store,
|
||||
}),
|
||||
AccessMode::DepthAttachment { load, store, .. } => {
|
||||
depth = Some(DepthStencilAttachmentPlan {
|
||||
resource: access.resource,
|
||||
load,
|
||||
store,
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
colors.sort_by_key(|color| color.location);
|
||||
(colors, depth)
|
||||
}
|
||||
|
||||
pub(crate) fn build_render_passes(
|
||||
executions: &[CompiledExecution],
|
||||
resources: &[CompiledResource],
|
||||
families: &[TextureFamily],
|
||||
) -> Vec<PhysicalRenderPass> {
|
||||
let family = |resource: u32| {
|
||||
resources
|
||||
.get(resource as usize)
|
||||
.and_then(|resource| match resource.plan {
|
||||
ResourcePlan::Texture { family, .. }
|
||||
| ResourcePlan::TextureSource { family, .. } => Some(family),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let mut passes: Vec<PhysicalRenderPass> = Vec::new();
|
||||
for (index, execution) in executions.iter().enumerate() {
|
||||
if matches!(execution.kind, ExecutionKind::FrameOut { .. }) {
|
||||
passes.push(PhysicalRenderPass {
|
||||
executions: vec![index as u32],
|
||||
kind: PhysicalRenderPassKind::Surface,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let (colors, depth) = execution_attachments(execution);
|
||||
let mut merged = false;
|
||||
if matches!(execution.kind, ExecutionKind::RasterDraw)
|
||||
&& colors.iter().all(|v| v.load == NormalizedColorLoad::Load)
|
||||
&& depth
|
||||
.as_ref()
|
||||
.is_none_or(|v| v.load == NormalizedDepthLoad::Load)
|
||||
{
|
||||
if let Some(PhysicalRenderPass {
|
||||
executions: members,
|
||||
kind:
|
||||
PhysicalRenderPassKind::Texture {
|
||||
color_attachments: previous_colors,
|
||||
depth_stencil: previous_depth,
|
||||
},
|
||||
}) = passes.last_mut()
|
||||
{
|
||||
let previous = &executions[*members.last().unwrap() as usize];
|
||||
let target_input = |socket: &str| {
|
||||
execution
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|input| input.socket == socket)
|
||||
.map(|input| input.resource)
|
||||
};
|
||||
let exact = previous.outputs.iter().any(|out| {
|
||||
out.socket == "color" && Some(out.resource) == target_input("colorTarget")
|
||||
}) && depth.as_ref().is_none_or(|_| {
|
||||
previous.outputs.iter().any(|out| {
|
||||
out.socket == "depth" && Some(out.resource) == target_input("depthTarget")
|
||||
})
|
||||
});
|
||||
let compatible = previous_colors.len() == colors.len()
|
||||
&& previous_colors.iter().zip(&colors).all(|(a, b)| {
|
||||
a.location == b.location
|
||||
&& family(a.resource) == family(b.resource)
|
||||
&& family(a.resource).is_some_and(|f| {
|
||||
family(b.resource).is_some_and(|next_family| {
|
||||
families.get(f as usize).is_some_and(|first| {
|
||||
families.get(next_family as usize).is_some_and(|next| {
|
||||
family_descriptor(first) == family_descriptor(next)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
&& match (previous_depth.as_ref(), depth.as_ref()) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => {
|
||||
family(a.resource) == family(b.resource)
|
||||
&& family(a.resource).is_some_and(|f| {
|
||||
family(b.resource).is_some_and(|next_family| {
|
||||
families.get(f as usize).is_some_and(|first| {
|
||||
families.get(next_family as usize).is_some_and(|next| {
|
||||
family_descriptor(first) == family_descriptor(next)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let no_resolve = previous_colors.iter().all(|v| v.resolve_target.is_none());
|
||||
let no_external = previous.outputs.iter().all(|out| {
|
||||
executions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, e)| e.inputs.iter().any(|v| v.resource == out.resource))
|
||||
.all(|(consumer, _)| consumer == index)
|
||||
});
|
||||
if exact && compatible && no_resolve && no_external {
|
||||
for (physical, final_value) in previous_colors.iter_mut().zip(&colors) {
|
||||
physical.resource = final_value.resource;
|
||||
physical.resolve_target = final_value.resolve_target;
|
||||
physical.store = final_value.store;
|
||||
}
|
||||
if let (Some(physical), Some(final_value)) =
|
||||
(previous_depth.as_mut(), depth.as_ref())
|
||||
{
|
||||
physical.resource = final_value.resource;
|
||||
physical.store = final_value.store;
|
||||
}
|
||||
members.push(index as u32);
|
||||
merged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !merged {
|
||||
passes.push(PhysicalRenderPass {
|
||||
executions: vec![index as u32],
|
||||
kind: PhysicalRenderPassKind::Texture {
|
||||
color_attachments: colors,
|
||||
depth_stencil: depth,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
passes
|
||||
}
|
||||
|
||||
fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
|
||||
match e {
|
||||
NormalizedTextureExtent::Absolute {
|
||||
|
||||
@@ -95,6 +95,11 @@ pub struct Contract {
|
||||
#[serde(skip)]
|
||||
pub fullscreen_policy: Option<FullscreenPolicy>,
|
||||
}
|
||||
impl Contract {
|
||||
pub const fn is_raster_draw(&self) -> bool {
|
||||
matches!(self.execution, ExecutionClass::Render) && self.fullscreen_policy.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
use SemanticType::*;
|
||||
const R: InputCardinality = InputCardinality { min: 1, max: 1 };
|
||||
@@ -138,7 +143,7 @@ const MESH_O: &[OutputSocketContract] = &[
|
||||
o("localAabb", LocalAabb),
|
||||
];
|
||||
const TEXTURE_O: &[OutputSocketContract] = &[o("texture", Texture)];
|
||||
const PIPE_I: &[InputSocketContract] = &[
|
||||
const RASTER_I: &[InputSocketContract] = &[
|
||||
i("mesh", MeshData, R, InputRole::SemanticRead),
|
||||
i("predicate", Bool, O, InputRole::Expression),
|
||||
i(
|
||||
@@ -149,7 +154,7 @@ const PIPE_I: &[InputSocketContract] = &[
|
||||
),
|
||||
i("depthTarget", Texture, O, InputRole::DepthTarget),
|
||||
];
|
||||
const PIPE_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
|
||||
const RASTER_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
|
||||
const CULL_I: &[InputSocketContract] = &[
|
||||
i("mesh", MeshData, R, InputRole::Expression),
|
||||
i("localAabb", LocalAabb, R, InputRole::Expression),
|
||||
@@ -202,7 +207,17 @@ 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),
|
||||
c!("ground_plane", 1, Render, RASTER_I, RASTER_O, false, None),
|
||||
c!("gltf_standard", 1, Render, RASTER_I, RASTER_O, false, None),
|
||||
c!(
|
||||
"gltf_standard_double_sided",
|
||||
1,
|
||||
Render,
|
||||
RASTER_I,
|
||||
RASTER_O,
|
||||
false,
|
||||
None
|
||||
),
|
||||
c!(
|
||||
"and",
|
||||
2,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Device-free render graph compiler and compiled graph registry.
|
||||
|
||||
mod compiler;
|
||||
pub(crate) use compiler::execution_attachments;
|
||||
mod contracts;
|
||||
mod expression;
|
||||
mod plan;
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct CompiledGraph {
|
||||
pub node_count: u32,
|
||||
pub resources: Vec<CompiledResource>,
|
||||
pub executions: Vec<CompiledExecution>,
|
||||
pub render_passes: Vec<PhysicalRenderPass>,
|
||||
pub texture_families: Vec<TextureFamily>,
|
||||
pub allocation_classes: Vec<AllocationClass>,
|
||||
pub culled_node_count: u32,
|
||||
@@ -108,16 +109,29 @@ pub struct CompiledSocketOutput {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ExecutionKind {
|
||||
Render {
|
||||
RasterDraw,
|
||||
Fullscreen,
|
||||
FrameOut { color: u32 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PhysicalRenderPass {
|
||||
pub executions: Vec<u32>,
|
||||
pub kind: PhysicalRenderPassKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum PhysicalRenderPassKind {
|
||||
Texture {
|
||||
color_attachments: Vec<ColorAttachmentPlan>,
|
||||
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
||||
},
|
||||
FrameOut {
|
||||
color: u32,
|
||||
},
|
||||
Surface,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ColorAttachmentPlan {
|
||||
pub resource: u32,
|
||||
@@ -127,7 +141,7 @@ pub struct ColorAttachmentPlan {
|
||||
pub store: StoreOp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DepthStencilAttachmentPlan {
|
||||
pub resource: u32,
|
||||
@@ -206,8 +220,8 @@ pub enum NormalizedParameters {
|
||||
ExpressionDefaults {
|
||||
defaults: Vec<TypedLiteral>,
|
||||
},
|
||||
Pipeline {
|
||||
pipeline: String,
|
||||
Raster {
|
||||
draw_order: i32,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
@@ -472,6 +486,6 @@ pub enum TextureUsage {
|
||||
|
||||
impl CompiledGraph {
|
||||
pub fn summary(&self, id: [u32; 2]) -> serde_json::Value {
|
||||
serde_json::json!({"compiledId":id,"graphId":self.graph_id,"revision":self.revision,"schemaVersion":self.schema_version,"nodeCount":self.node_count,"executionCount":self.executions.len(),"resourceCount":self.resources.len(),"culledNodeCount":self.culled_node_count,"culledResourceCount":self.culled_resource_count,"transientSlotCount":self.transient_slot_count})
|
||||
serde_json::json!({"compiledId":id,"graphId":self.graph_id,"revision":self.revision,"schemaVersion":self.schema_version,"nodeCount":self.node_count,"executionCount":self.executions.len(),"physicalPassCount":self.render_passes.len(),"resourceCount":self.resources.len(),"culledNodeCount":self.culled_node_count,"culledResourceCount":self.culled_resource_count,"transientSlotCount":self.transient_slot_count})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ pub struct RuntimeAllocationPlan {
|
||||
pub struct RuntimePlan {
|
||||
pub allocations: RuntimeAllocationPlan,
|
||||
pub executions: Vec<RuntimeExecution>,
|
||||
pub render_passes: Vec<PhysicalRenderPass>,
|
||||
pub instance_traversal: Option<InstanceTraversalPlan>,
|
||||
pub surface: RuntimeSurfaceContract,
|
||||
}
|
||||
@@ -365,17 +366,9 @@ fn invalid(message: impl Into<String>, path: impl Into<String>) -> GraphError {
|
||||
error("GRAPH_RUNTIME_PLAN_INVALID", message, path)
|
||||
}
|
||||
|
||||
fn valid_pipeline_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name.len() <= 64
|
||||
&& name.bytes().enumerate().all(|(i, byte)| {
|
||||
byte == b'_' || byte.is_ascii_alphanumeric() && (i > 0 || byte.is_ascii_alphabetic())
|
||||
})
|
||||
}
|
||||
|
||||
fn execution_supported(key: &str) -> bool {
|
||||
contract(key).is_some_and(|contract| {
|
||||
contract.fullscreen_policy.is_some() || matches!(key, "pipeline" | "frame_out")
|
||||
contract.fullscreen_policy.is_some() || contract.is_raster_draw() || key == "frame_out"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -524,13 +517,13 @@ fn validate_fullscreen_execution(
|
||||
if output.socket != "color" {
|
||||
return Err(invalid("fullscreen outputs mismatch", path("outputs")));
|
||||
}
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
depth_stencil: None,
|
||||
} = &execution.kind
|
||||
else {
|
||||
if !matches!(execution.kind, ExecutionKind::Fullscreen) {
|
||||
return Err(invalid("fullscreen render kind mismatch", path("kind")));
|
||||
};
|
||||
}
|
||||
let (color_attachments, depth_stencil) = super::compiler::execution_attachments(execution);
|
||||
if depth_stencil.is_some() {
|
||||
return Err(invalid("fullscreen depth mismatch", path("kind")));
|
||||
}
|
||||
let [attachment] = color_attachments.as_slice() else {
|
||||
return Err(invalid("fullscreen attachment mismatch", path("kind")));
|
||||
};
|
||||
@@ -749,7 +742,7 @@ fn validate_pipeline_resolve(
|
||||
.filter(|access| matches!(access.mode, AccessMode::ColorResolve { .. }))
|
||||
.count()
|
||||
== 1;
|
||||
if producer.executor.key != "pipeline"
|
||||
if !contract(&producer.executor.key).is_some_and(Contract::is_raster_draw)
|
||||
|| producer.original_node_index != *producer_node_index
|
||||
|| !exact_output
|
||||
|| !matches!(&source.origin,
|
||||
@@ -808,6 +801,123 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
"schemaVersion",
|
||||
));
|
||||
}
|
||||
let mut expected_execution = 0usize;
|
||||
for (pass_index, pass) in graph.render_passes.iter().enumerate() {
|
||||
if pass.executions.is_empty() {
|
||||
return Err(invalid(
|
||||
"physical pass is empty",
|
||||
format!("renderPasses[{pass_index}]"),
|
||||
));
|
||||
}
|
||||
for &member in &pass.executions {
|
||||
if member as usize != expected_execution || expected_execution >= graph.executions.len()
|
||||
{
|
||||
return Err(invalid(
|
||||
"physical passes must partition logical executions in order",
|
||||
format!("renderPasses[{pass_index}].executions"),
|
||||
));
|
||||
}
|
||||
expected_execution += 1;
|
||||
}
|
||||
let singleton = pass.executions.len() == 1;
|
||||
let kinds_valid = match &pass.kind {
|
||||
PhysicalRenderPassKind::Surface => {
|
||||
singleton
|
||||
&& matches!(
|
||||
graph.executions[pass.executions[0] as usize].kind,
|
||||
ExecutionKind::FrameOut { .. }
|
||||
)
|
||||
}
|
||||
PhysicalRenderPassKind::Texture {
|
||||
color_attachments,
|
||||
depth_stencil,
|
||||
} => {
|
||||
pass.executions.iter().all(|&member| {
|
||||
!matches!(
|
||||
graph.executions[member as usize].kind,
|
||||
ExecutionKind::FrameOut { .. }
|
||||
)
|
||||
}) && (singleton
|
||||
|| pass.executions.iter().all(|&member| {
|
||||
matches!(
|
||||
graph.executions[member as usize].kind,
|
||||
ExecutionKind::RasterDraw
|
||||
)
|
||||
}))
|
||||
&& color_attachments.iter().all(|attachment| {
|
||||
(attachment.resource as usize) < graph.resources.len()
|
||||
&& attachment
|
||||
.resolve_target
|
||||
.is_none_or(|resource| (resource as usize) < graph.resources.len())
|
||||
})
|
||||
&& depth_stencil.as_ref().is_none_or(|attachment| {
|
||||
(attachment.resource as usize) < graph.resources.len()
|
||||
})
|
||||
}
|
||||
};
|
||||
if !kinds_valid {
|
||||
return Err(invalid(
|
||||
"physical pass kind or attachment is invalid",
|
||||
format!("renderPasses[{pass_index}]"),
|
||||
));
|
||||
}
|
||||
}
|
||||
if expected_execution != graph.executions.len() {
|
||||
return Err(invalid(
|
||||
"physical passes omit logical executions",
|
||||
"renderPasses",
|
||||
));
|
||||
}
|
||||
for (resource_index, resource) in graph.resources.iter().enumerate() {
|
||||
if let ResourcePlan::Texture { family, .. } | ResourcePlan::TextureSource { family, .. } =
|
||||
resource.plan
|
||||
{
|
||||
if family as usize >= graph.texture_families.len() {
|
||||
return Err(invalid(
|
||||
"texture family is out of bounds",
|
||||
format!("resources[{resource_index}].plan.family"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let canonical_passes = super::compiler::build_render_passes(
|
||||
&graph.executions,
|
||||
&graph.resources,
|
||||
&graph.texture_families,
|
||||
);
|
||||
if graph.render_passes != canonical_passes {
|
||||
return Err(invalid(
|
||||
"physical render pass plan is not canonical",
|
||||
"renderPasses",
|
||||
));
|
||||
}
|
||||
for (pass_index, pass) in graph.render_passes.iter().enumerate() {
|
||||
if !matches!(pass.kind, PhysicalRenderPassKind::Texture { .. }) {
|
||||
continue;
|
||||
}
|
||||
let mut previous = None;
|
||||
for &member in &pass.executions {
|
||||
let execution = &graph.executions[member as usize];
|
||||
let NormalizedParameters::Raster { draw_order, .. } = &execution.parameters else {
|
||||
previous = None;
|
||||
continue;
|
||||
};
|
||||
let key = (*draw_order, execution.original_node_index);
|
||||
if previous.is_some_and(|value| value > key) {
|
||||
return Err(invalid(
|
||||
"raster pass members are not in canonical draw order",
|
||||
format!("renderPasses[{pass_index}].executions"),
|
||||
));
|
||||
}
|
||||
previous = Some(key);
|
||||
}
|
||||
}
|
||||
let execution_pass: Vec<u32> = graph
|
||||
.render_passes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(pass, value)| value.executions.iter().map(move |_| pass as u32))
|
||||
.collect();
|
||||
|
||||
let mut producers = vec![None; graph.resources.len()];
|
||||
let mut uses = vec![BTreeSet::new(); graph.resources.len()];
|
||||
@@ -878,7 +988,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
format!("executions[{i}]"),
|
||||
)
|
||||
})?
|
||||
.insert(i as u32);
|
||||
.insert(execution_pass[i]);
|
||||
}
|
||||
}
|
||||
for (ri, resource) in graph.resources.iter().enumerate() {
|
||||
@@ -910,6 +1020,52 @@ 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
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|candidate| candidate.name == input.socket)
|
||||
.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)
|
||||
{
|
||||
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"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1004,8 +1160,9 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
.count()
|
||||
== 1
|
||||
})
|
||||
&& owner_executions[0].executor.key == "pipeline"
|
||||
&& owner_executions[0].executor.version == 4
|
||||
&& contract(&owner_executions[0].executor.key)
|
||||
.is_some_and(Contract::is_raster_draw)
|
||||
&& owner_executions[0].executor.version == 1
|
||||
&& graph
|
||||
.executions
|
||||
.iter()
|
||||
@@ -1013,7 +1170,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
.filter(|input| input.resource == source)
|
||||
.count()
|
||||
== 1
|
||||
&& contract("pipeline")
|
||||
&& contract(&owner_executions[0].executor.key)
|
||||
.and_then(|contract| contract.inputs.get(*input_ordinal as usize))
|
||||
.is_some_and(|input| {
|
||||
input.name == *socket
|
||||
@@ -1242,7 +1399,11 @@ fn validate_instance_traversal(graph: &CompiledGraph) -> Result<(), GraphError>
|
||||
.executions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, e)| (e.executor.key == "pipeline").then_some(i as u32))
|
||||
.filter_map(|(i, e)| {
|
||||
contract(&e.executor.key)
|
||||
.is_some_and(Contract::is_raster_draw)
|
||||
.then_some(i as u32)
|
||||
})
|
||||
.collect();
|
||||
let Some(plan) = &graph.instance_traversal else {
|
||||
return if pipeline_indices.is_empty() {
|
||||
@@ -1552,7 +1713,7 @@ pub fn prepare_runtime_plan(
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
let path = format!("executions[{i}]");
|
||||
match execution.executor.key.as_str() {
|
||||
"pipeline" => {}
|
||||
key if contract(key).is_some_and(Contract::is_raster_draw) => {}
|
||||
_ if contract(&execution.executor.key)
|
||||
.is_some_and(|contract| contract.fullscreen_policy.is_some()) => {}
|
||||
"frame_out" => {
|
||||
@@ -1587,11 +1748,11 @@ pub fn prepare_runtime_plan(
|
||||
validate_instance_traversal(graph)?;
|
||||
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
if execution.executor.key != "pipeline" {
|
||||
if !contract(&execution.executor.key).is_some_and(Contract::is_raster_draw) {
|
||||
continue;
|
||||
}
|
||||
let NormalizedParameters::Pipeline {
|
||||
pipeline,
|
||||
let NormalizedParameters::Raster {
|
||||
draw_order: _,
|
||||
clear_depth,
|
||||
clear_color,
|
||||
..
|
||||
@@ -1602,8 +1763,7 @@ pub fn prepare_runtime_plan(
|
||||
format!("executions[{i}].parameters"),
|
||||
));
|
||||
};
|
||||
if !valid_pipeline_name(pipeline)
|
||||
|| !clear_depth.is_finite()
|
||||
if !clear_depth.is_finite()
|
||||
|| !(0.0..=1.0).contains(clear_depth)
|
||||
|| clear_color.iter().any(|value| !value.is_finite())
|
||||
{
|
||||
@@ -1612,15 +1772,18 @@ pub fn prepare_runtime_plan(
|
||||
format!("executions[{i}].parameters"),
|
||||
));
|
||||
}
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
depth_stencil: Some(depth_attachment),
|
||||
} = &execution.kind
|
||||
else {
|
||||
if !matches!(execution.kind, ExecutionKind::RasterDraw) {
|
||||
return Err(invalid(
|
||||
"pipeline render kind mismatch",
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
}
|
||||
let (color_attachments, depth_stencil) = super::compiler::execution_attachments(execution);
|
||||
let Some(depth_attachment) = depth_stencil.as_ref() else {
|
||||
return Err(invalid(
|
||||
"pipeline depth attachment missing",
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
};
|
||||
let [color_attachment] = color_attachments.as_slice() else {
|
||||
return Err(invalid(
|
||||
@@ -2307,6 +2470,7 @@ pub fn prepare_runtime_plan(
|
||||
resource_allocations,
|
||||
},
|
||||
executions,
|
||||
render_passes: graph.render_passes.clone(),
|
||||
instance_traversal: graph.instance_traversal.clone(),
|
||||
surface,
|
||||
})
|
||||
|
||||
@@ -49,8 +49,8 @@ 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", "pipeline", 4,
|
||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
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("frame", "frame_out", 3,
|
||||
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,
|
||||
@@ -62,7 +62,16 @@ pub(crate) fn full_cull_graph() -> Value {
|
||||
#[test]
|
||||
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
||||
assert_eq!(contract("mesh").unwrap().version, 2);
|
||||
assert_eq!(contract("pipeline").unwrap().version, 4);
|
||||
assert!(contract("pipeline").is_none());
|
||||
for key in [
|
||||
"ground_plane",
|
||||
"gltf_standard",
|
||||
"gltf_standard_double_sided",
|
||||
] {
|
||||
let contract = contract(key).unwrap();
|
||||
assert_eq!(contract.version, 1);
|
||||
assert!(contract.is_raster_draw());
|
||||
}
|
||||
assert_eq!(
|
||||
contract("mesh")
|
||||
.unwrap()
|
||||
@@ -76,7 +85,7 @@ fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
||||
("localAabb", SemanticType::LocalAabb)
|
||||
]
|
||||
);
|
||||
let predicate = contract("pipeline")
|
||||
let predicate = contract("gltf_standard")
|
||||
.unwrap()
|
||||
.inputs
|
||||
.iter()
|
||||
@@ -348,7 +357,7 @@ fn pipeline_predicate_defaults_true_and_expression_edges_are_validated() {
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
pipeline.parameters,
|
||||
NormalizedParameters::Pipeline {
|
||||
NormalizedParameters::Raster {
|
||||
predicate_default: true,
|
||||
..
|
||||
}
|
||||
@@ -359,6 +368,93 @@ 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();
|
||||
graph["nodes"][8]["parameters"]["pipeline"] = json!("gltf_standard");
|
||||
let error = compile_value(graph).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_PARAMETERS_INVALID");
|
||||
assert_eq!(error.details["path"], "nodes[8].parameters");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_generic_pipeline_executor_is_unknown() {
|
||||
let mut graph = full_cull_graph();
|
||||
graph["nodes"][8]["executor"] = json!({"key":"pipeline","version":4});
|
||||
let error = compile_value(graph).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_UNKNOWN_EXECUTOR");
|
||||
assert_eq!(error.details["path"], "nodes[8].executor.key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_raster_writers_form_one_ordered_physical_pass() {
|
||||
let mut graph = full_cull_graph();
|
||||
let nodes = graph["nodes"].as_array_mut().unwrap();
|
||||
nodes.insert(
|
||||
9,
|
||||
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")}),
|
||||
),
|
||||
);
|
||||
nodes.insert(10, texture("composite", "rgba16_float"));
|
||||
nodes.insert(
|
||||
11,
|
||||
node(
|
||||
"combine",
|
||||
"bloom_composite",
|
||||
1,
|
||||
json!({"intensity":1.0}),
|
||||
json!({"source":input("pipeline","color"),"bloom":input("sibling","color"),"colorTarget":input("composite","texture")}),
|
||||
),
|
||||
);
|
||||
nodes[12]["inputs"]["color"] = input("combine", "color");
|
||||
let compiled = compile_value(graph).unwrap();
|
||||
assert_eq!(
|
||||
compiled
|
||||
.executions
|
||||
.iter()
|
||||
.map(|execution| execution.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["pipeline", "sibling", "combine", "frame"]
|
||||
);
|
||||
assert_eq!(compiled.render_passes[0].executions, [0, 1]);
|
||||
assert!(matches!(
|
||||
compiled.render_passes[0].kind,
|
||||
PhysicalRenderPassKind::Texture { .. }
|
||||
));
|
||||
assert_eq!(compiled.texture_families[0].versions.len(), 2);
|
||||
assert_eq!(compiled.texture_families[1].versions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expression_provenance_rejects_cross_mesh_values() {
|
||||
let mut graph = full_cull_graph();
|
||||
@@ -375,11 +471,11 @@ 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", "pipeline", 4,
|
||||
json!({"pipeline":"ground_plane","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
node("first", "ground_plane", 1,
|
||||
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
json!({"mesh":input("mesh","mesh")})),
|
||||
node("second", "pipeline", 4,
|
||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||
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("frame", "frame_out", 3,
|
||||
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]}),
|
||||
@@ -387,10 +483,294 @@ 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.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")}),
|
||||
),
|
||||
);
|
||||
nodes.insert(
|
||||
10,
|
||||
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")}),
|
||||
),
|
||||
);
|
||||
nodes[11]["inputs"]["color"] = input("double", "color");
|
||||
graph
|
||||
}
|
||||
|
||||
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"][11]["inputs"]["color"] = input(frame_source, "color");
|
||||
graph
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_raster_outputs_all_observe_the_terminal_cohort() {
|
||||
let mut terminal = None;
|
||||
for source in ["pipeline", "standard", "double"] {
|
||||
let graph = compile_value(direct_raster_graph(source)).unwrap();
|
||||
assert_eq!(
|
||||
graph
|
||||
.executions
|
||||
.iter()
|
||||
.map(|execution| execution.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["double", "standard", "pipeline", "frame"]
|
||||
);
|
||||
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
|
||||
let frame_color = match graph.executions[3].kind {
|
||||
ExecutionKind::FrameOut { color } => color,
|
||||
_ => panic!("expected frame output"),
|
||||
};
|
||||
assert_eq!(*terminal.get_or_insert(frame_color), frame_color);
|
||||
let producers: Vec<_> = graph.executions[..3]
|
||||
.iter()
|
||||
.flat_map(|execution| execution.outputs.iter().map(|output| output.resource))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
producers
|
||||
.iter()
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.len(),
|
||||
6
|
||||
);
|
||||
assert!(validate_activatable(&graph).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert_eq!(
|
||||
graph.executions[..3]
|
||||
.iter()
|
||||
.map(|execution| execution.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["pipeline", "standard", "double"]
|
||||
);
|
||||
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dead_fullscreen_attachment_writer_of_observed_raster_output_stays_dead() {
|
||||
let mut value = direct_raster_graph("pipeline");
|
||||
value["nodes"].as_array_mut().unwrap().insert(
|
||||
11,
|
||||
node(
|
||||
"dead_writer",
|
||||
"fullscreen_copy",
|
||||
1,
|
||||
json!({}),
|
||||
json!({
|
||||
"source": input("color", "texture"),
|
||||
"colorTarget": input("pipeline", "color")
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert!(!graph
|
||||
.executions
|
||||
.iter()
|
||||
.any(|execution| execution.id == "dead_writer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dead_cyclic_fullscreen_reader_does_not_become_live_through_war() {
|
||||
let mut value = direct_raster_graph("pipeline");
|
||||
value["nodes"].as_array_mut().unwrap().insert(
|
||||
11,
|
||||
node(
|
||||
"dead_reader",
|
||||
"fullscreen_copy",
|
||||
1,
|
||||
json!({}),
|
||||
json!({
|
||||
"source": input("pipeline", "color"),
|
||||
"colorTarget": input("dead_reader", "color")
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert!(!graph
|
||||
.executions
|
||||
.iter()
|
||||
.any(|execution| execution.id == "dead_reader"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_raster_chain_compiles_to_one_physical_pass() {
|
||||
let graph = compile_value(three_raster_graph()).unwrap();
|
||||
assert_eq!(graph.executions.len(), 4);
|
||||
assert!(graph.executions[..3]
|
||||
.iter()
|
||||
.all(|execution| matches!(execution.kind, ExecutionKind::RasterDraw)));
|
||||
assert_eq!(graph.render_passes.len(), 2);
|
||||
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
|
||||
assert_eq!(graph.render_passes[1].executions, [3]);
|
||||
let PhysicalRenderPassKind::Texture {
|
||||
color_attachments,
|
||||
depth_stencil: Some(depth),
|
||||
} = &graph.render_passes[0].kind
|
||||
else {
|
||||
panic!("expected raster pass")
|
||||
};
|
||||
let first = execution_attachments(&graph.executions[0]);
|
||||
let final_attachments = execution_attachments(&graph.executions[2]);
|
||||
assert_eq!(color_attachments[0].load, first.0[0].load);
|
||||
assert_eq!(
|
||||
color_attachments[0].resource,
|
||||
final_attachments.0[0].resource
|
||||
);
|
||||
assert_eq!(color_attachments[0].store, final_attachments.0[0].store);
|
||||
assert_eq!(
|
||||
depth.resource,
|
||||
final_attachments.1.as_ref().unwrap().resource
|
||||
);
|
||||
assert_eq!(depth.store, final_attachments.1.as_ref().unwrap().store);
|
||||
for execution in &graph.executions[1..3] {
|
||||
let (color, depth) = execution_attachments(execution);
|
||||
assert_eq!(color[0].load, NormalizedColorLoad::Load);
|
||||
assert_eq!(depth.unwrap().load, NormalizedDepthLoad::Load);
|
||||
}
|
||||
for socket in ["color", "depth"] {
|
||||
let resources: Vec<_> = graph.executions[..3]
|
||||
.iter()
|
||||
.map(|execution| {
|
||||
execution
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| output.socket == socket)
|
||||
.unwrap()
|
||||
.resource
|
||||
})
|
||||
.collect();
|
||||
let family = |resource| match graph.resources[resource as usize].plan {
|
||||
ResourcePlan::Texture { family, .. } => family,
|
||||
_ => panic!("expected texture version"),
|
||||
};
|
||||
let allocation = |resource| match graph.resources[resource as usize].plan {
|
||||
ResourcePlan::Texture { allocation, .. } => allocation,
|
||||
_ => panic!("expected texture version"),
|
||||
};
|
||||
assert!(resources
|
||||
.iter()
|
||||
.all(|&resource| family(resource) == family(resources[0])));
|
||||
assert!(resources
|
||||
.iter()
|
||||
.all(|&resource| allocation(resource) == allocation(resources[0])));
|
||||
assert!(resources[..2]
|
||||
.iter()
|
||||
.all(|&resource| graph.resources[resource as usize].lifetime
|
||||
== Some(Lifetime {
|
||||
first_use: 0,
|
||||
last_use: 0
|
||||
})));
|
||||
}
|
||||
let traversal = graph.instance_traversal.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
traversal
|
||||
.pipelines
|
||||
.iter()
|
||||
.map(|pipeline| (pipeline.execution, pipeline.ordinal))
|
||||
.collect::<Vec<_>>(),
|
||||
[(0, 0), (1, 1), (2, 2)]
|
||||
);
|
||||
assert!(validate_activatable(&graph).is_ok());
|
||||
assert_eq!(
|
||||
serde_json::to_value(&graph).unwrap(),
|
||||
serde_json::to_value(compile_value(three_raster_graph()).unwrap()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_msaa_resolve_stays_on_merged_raster_boundary() {
|
||||
let mut value = three_raster_graph();
|
||||
set_sample_count(&mut value["nodes"][0], 4);
|
||||
set_sample_count(&mut value["nodes"][1], 4);
|
||||
let graph = compile_value(value).unwrap();
|
||||
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
|
||||
let PhysicalRenderPassKind::Texture {
|
||||
color_attachments, ..
|
||||
} = &graph.render_passes[0].kind
|
||||
else {
|
||||
panic!("expected texture pass")
|
||||
};
|
||||
assert!(color_attachments[0].resolve_target.is_some());
|
||||
assert!(execution_attachments(&graph.executions[0]).0[0]
|
||||
.resolve_target
|
||||
.is_none());
|
||||
assert!(execution_attachments(&graph.executions[1]).0[0]
|
||||
.resolve_target
|
||||
.is_none());
|
||||
assert!(validate_activatable(&graph).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_rejects_noncanonical_physical_passes() {
|
||||
let graph = compile_value(three_raster_graph()).unwrap();
|
||||
for mutate in [
|
||||
|graph: &mut CompiledGraph| {
|
||||
graph.render_passes[0].executions.pop();
|
||||
},
|
||||
|graph: &mut CompiledGraph| {
|
||||
graph.render_passes[0].executions.push(1);
|
||||
},
|
||||
|graph: &mut CompiledGraph| {
|
||||
graph.render_passes[0].executions.swap(0, 1);
|
||||
},
|
||||
] {
|
||||
let mut invalid = graph.clone();
|
||||
mutate(&mut invalid);
|
||||
assert_runtime_plan_invalid(&invalid);
|
||||
}
|
||||
let mut wrong_resource = graph.clone();
|
||||
let first_color = execution_attachments(&wrong_resource.executions[0]).0[0].resource;
|
||||
let PhysicalRenderPassKind::Texture {
|
||||
color_attachments, ..
|
||||
} = &mut wrong_resource.render_passes[0].kind
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
color_attachments[0].resource = first_color;
|
||||
assert_runtime_plan_invalid(&wrong_resource);
|
||||
|
||||
let mut wrong_lifetime = graph;
|
||||
let intermediate = wrong_lifetime.executions[1].outputs[0].resource;
|
||||
wrong_lifetime.resources[intermediate as usize].lifetime = Some(Lifetime {
|
||||
first_use: 1,
|
||||
last_use: 1,
|
||||
});
|
||||
assert_runtime_plan_invalid(&wrong_lifetime);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contract_v4_declares_strict_default_policies() {
|
||||
let pipeline = contract("pipeline").unwrap();
|
||||
assert_eq!(pipeline.version, 4);
|
||||
let pipeline = contract("gltf_standard").unwrap();
|
||||
assert_eq!(pipeline.version, 1);
|
||||
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
|
||||
assert_eq!(
|
||||
pipeline.inputs[1].default_policy,
|
||||
@@ -445,13 +825,9 @@ fn disconnected_targets_have_tagged_roots_and_clear_version_zero() {
|
||||
.iter()
|
||||
.find(|e| e.id == "first")
|
||||
.unwrap();
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
depth_stencil: Some(depth),
|
||||
} = &first.kind
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(first.kind, ExecutionKind::RasterDraw));
|
||||
let (color_attachments, depth_stencil) = execution_attachments(first);
|
||||
let depth = depth_stencil.unwrap();
|
||||
assert!(matches!(
|
||||
color_attachments[0].load,
|
||||
NormalizedColorLoad::Clear { .. }
|
||||
@@ -468,13 +844,9 @@ fn implicit_chain_is_deterministic_and_loads_successors() {
|
||||
serde_json::to_value(&b).unwrap()
|
||||
);
|
||||
let second = a.executions.iter().find(|e| e.id == "second").unwrap();
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
depth_stencil: Some(depth),
|
||||
} = &second.kind
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(second.kind, ExecutionKind::RasterDraw));
|
||||
let (color_attachments, depth_stencil) = execution_attachments(second);
|
||||
let depth = depth_stencil.unwrap();
|
||||
assert_eq!(color_attachments[0].load, NormalizedColorLoad::Load);
|
||||
assert_eq!(depth.load, NormalizedDepthLoad::Load);
|
||||
}
|
||||
@@ -586,7 +958,8 @@ fn descriptor_dependency_cycle_keeps_graph_cycle_priority() {
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("depthTarget");
|
||||
assert_eq!(compile_value(graph).unwrap_err().code, "GRAPH_CYCLE");
|
||||
// The backwards authored attachment is normalized by draw order.
|
||||
assert!(compile_value(graph).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -595,8 +968,8 @@ fn known_attachment_error_precedes_cycle() {
|
||||
graph["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
||||
graph["nodes"][8]["inputs"]["depthTarget"] = input("pipeline", "depth");
|
||||
let error = compile_value(graph).unwrap_err();
|
||||
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs.colorTarget");
|
||||
assert_eq!(error.code, "GRAPH_ATTACHMENT_LINEAGE_INVALID");
|
||||
assert_eq!(error.details["path"], "nodes[8].inputs");
|
||||
}
|
||||
|
||||
fn self_targeting_copy(source_format: &str) -> Value {
|
||||
@@ -633,6 +1006,57 @@ fn known_invalid_fullscreen_source_precedes_target_cycle() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_output_is_a_valid_raster_attachment_root() {
|
||||
let mut graph = full_cull_graph();
|
||||
let nodes = graph["nodes"].as_array_mut().unwrap();
|
||||
nodes.insert(9, texture("copy_target", "rgba16_float"));
|
||||
nodes.insert(
|
||||
10,
|
||||
node(
|
||||
"copy",
|
||||
"fullscreen_copy",
|
||||
1,
|
||||
json!({}),
|
||||
json!({
|
||||
"source": input("pipeline", "color"),
|
||||
"colorTarget": input("copy_target", "texture")
|
||||
}),
|
||||
),
|
||||
);
|
||||
nodes.insert(
|
||||
11,
|
||||
node(
|
||||
"later",
|
||||
"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("copy", "color"),
|
||||
"depthTarget": input("pipeline", "depth")
|
||||
}),
|
||||
),
|
||||
);
|
||||
nodes[12]["inputs"]["color"] = input("later", "color");
|
||||
|
||||
let compiled = compile_value(graph).unwrap();
|
||||
assert_eq!(
|
||||
compiled
|
||||
.executions
|
||||
.iter()
|
||||
.map(|execution| execution.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["pipeline", "copy", "later", "frame"]
|
||||
);
|
||||
assert_eq!(compiled.render_passes.len(), 4);
|
||||
assert!(compiled
|
||||
.render_passes
|
||||
.iter()
|
||||
.all(|pass| pass.executions.len() == 1));
|
||||
assert!(validate_activatable(&compiled).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_invalid_fullscreen_target_precedes_source_cycle() {
|
||||
let mut graph = implicit_pipeline_graph();
|
||||
|
||||
@@ -54,7 +54,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
indirect_commands: &wgpu::Buffer,
|
||||
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||
) -> Result<(), &'static str> {
|
||||
use crate::render_graph::{ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
|
||||
use crate::render_graph::{NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
|
||||
let view = |resource: u32| -> Result<&wgpu::TextureView, &'static str> {
|
||||
let a = active
|
||||
.runtime
|
||||
@@ -71,104 +71,55 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
.map(|s| &s.view)
|
||||
.ok_or(" allocation out of bounds")
|
||||
};
|
||||
for prepared in &active.executions {
|
||||
match prepared {
|
||||
PreparedExecution::Fullscreen {
|
||||
execution,
|
||||
frame_out,
|
||||
bind_group,
|
||||
pipeline,
|
||||
..
|
||||
} => {
|
||||
let execution = active
|
||||
for physical in &active.runtime.render_passes {
|
||||
let first = *physical.executions.first().ok_or("empty physical pass")? as usize;
|
||||
let last = *physical.executions.last().ok_or("empty physical pass")? as usize;
|
||||
let label = if first == last {
|
||||
active
|
||||
.graph
|
||||
.executions
|
||||
.get(*execution)
|
||||
.ok_or(" execution out of bounds")?;
|
||||
let (target, operations) = if *frame_out {
|
||||
let ExecutionKind::FrameOut { .. } = execution.kind else {
|
||||
return Err("frame_out kind mismatch");
|
||||
.get(first)
|
||||
.ok_or("execution out of bounds")?
|
||||
.id
|
||||
.clone()
|
||||
} else {
|
||||
format!(
|
||||
"{}..{}",
|
||||
active
|
||||
.graph
|
||||
.executions
|
||||
.get(first)
|
||||
.ok_or("execution out of bounds")?
|
||||
.id,
|
||||
active
|
||||
.graph
|
||||
.executions
|
||||
.get(last)
|
||||
.ok_or("execution out of bounds")?
|
||||
.id
|
||||
)
|
||||
};
|
||||
(
|
||||
surface,
|
||||
wgpu::Operations {
|
||||
let (colors, depth) = match &physical.kind {
|
||||
crate::render_graph::PhysicalRenderPassKind::Surface => (
|
||||
vec![Some(wgpu::RenderPassColorAttachment {
|
||||
view: surface,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments, ..
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err("fullscreen is not render");
|
||||
};
|
||||
let color = color_attachments
|
||||
.first()
|
||||
.ok_or("fullscreen target missing")?;
|
||||
(
|
||||
view(color.resource)?,
|
||||
wgpu::Operations {
|
||||
load: match color.load {
|
||||
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
|
||||
NormalizedColorLoad::Clear { value } => {
|
||||
wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: value[0],
|
||||
g: value[1],
|
||||
b: value[2],
|
||||
a: value[3],
|
||||
})
|
||||
}
|
||||
},
|
||||
store: if color.store == StoreOp::Store {
|
||||
wgpu::StoreOp::Store
|
||||
} else {
|
||||
wgpu::StoreOp::Discard
|
||||
},
|
||||
},
|
||||
)
|
||||
};
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&execution.id),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: operations,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: profile
|
||||
.as_deref_mut()
|
||||
.and_then(|p| p.render_writes(&execution.id)),
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
PreparedExecution::Pipeline {
|
||||
execution,
|
||||
base,
|
||||
predicate_ordinal,
|
||||
variant,
|
||||
} => {
|
||||
let execution = active
|
||||
.graph
|
||||
.executions
|
||||
.get(*execution)
|
||||
.ok_or(" execution out of bounds")?;
|
||||
let ExecutionKind::Render {
|
||||
None,
|
||||
),
|
||||
crate::render_graph::PhysicalRenderPassKind::Texture {
|
||||
color_attachments,
|
||||
depth_stencil,
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err("pipeline is not render");
|
||||
};
|
||||
let color = color_attachments.first().ok_or("pipeline color missing")?;
|
||||
let depth = depth_stencil.as_ref().ok_or("pipeline depth missing")?;
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&execution.id),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
} => {
|
||||
let colors = color_attachments
|
||||
.iter()
|
||||
.map(|color| {
|
||||
Ok(Some(wgpu::RenderPassColorAttachment {
|
||||
view: view(color.resource)?,
|
||||
depth_slice: None,
|
||||
resolve_target: color.resolve_target.map(view).transpose()?,
|
||||
@@ -190,13 +141,20 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
wgpu::StoreOp::Discard
|
||||
},
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
}))
|
||||
})
|
||||
.collect::<Result<Vec<_>, &'static str>>()?;
|
||||
let depth = depth_stencil
|
||||
.as_ref()
|
||||
.map(|depth| -> Result<_, &'static str> {
|
||||
Ok(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: view(depth.resource)?,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: match depth.load {
|
||||
NormalizedDepthLoad::Load => wgpu::LoadOp::Load,
|
||||
NormalizedDepthLoad::Clear { value } => wgpu::LoadOp::Clear(value),
|
||||
NormalizedDepthLoad::Clear { value } => {
|
||||
wgpu::LoadOp::Clear(value)
|
||||
}
|
||||
},
|
||||
store: if depth.store == StoreOp::Store {
|
||||
wgpu::StoreOp::Store
|
||||
@@ -205,12 +163,40 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
},
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
(colors, depth)
|
||||
}
|
||||
};
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&label),
|
||||
color_attachments: &colors,
|
||||
depth_stencil_attachment: depth,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: profile
|
||||
.as_deref_mut()
|
||||
.and_then(|p| p.render_writes(&execution.id)),
|
||||
timestamp_writes: profile.as_deref_mut().and_then(|p| p.render_writes(&label)),
|
||||
});
|
||||
for &member in &physical.executions {
|
||||
match active
|
||||
.executions
|
||||
.get(member as usize)
|
||||
.ok_or("prepared execution out of bounds")?
|
||||
{
|
||||
PreparedExecution::Fullscreen {
|
||||
bind_group,
|
||||
pipeline,
|
||||
..
|
||||
} => {
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_bind_group(0, bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
PreparedExecution::Pipeline {
|
||||
base,
|
||||
predicate_ordinal,
|
||||
variant,
|
||||
..
|
||||
} => {
|
||||
for (i, group) in scene.bind_groups().iter().enumerate() {
|
||||
pass.set_bind_group(i as u32, group, &[]);
|
||||
}
|
||||
@@ -246,6 +232,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -815,14 +815,11 @@ struct GpuTextureSlot {
|
||||
|
||||
enum PreparedExecution {
|
||||
Pipeline {
|
||||
execution: usize,
|
||||
base: crate::render_data::PipelineKey,
|
||||
predicate_ordinal: u32,
|
||||
variant: wgpu::RenderPipeline,
|
||||
},
|
||||
Fullscreen {
|
||||
execution: usize,
|
||||
frame_out: bool,
|
||||
bind_group: wgpu::BindGroup,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
_uniform: wgpu::Buffer,
|
||||
@@ -1687,17 +1684,17 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, execution)| {
|
||||
let NormalizedParameters::Pipeline { pipeline, .. } = &execution.parameters else {
|
||||
let NormalizedParameters::Raster { .. } = &execution.parameters else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.resources
|
||||
.find_pipeline(pipeline)
|
||||
.find_pipeline(&execution.executor.key)
|
||||
.map(Some)
|
||||
.ok_or_else(|| {
|
||||
GraphError::at(
|
||||
"GRAPH_EXECUTION_UNSUPPORTED",
|
||||
format!("pipeline '{pipeline}' is not registered"),
|
||||
format!("executions[{index}].parameters.pipeline"),
|
||||
format!("pipeline '{}' is not registered", execution.executor.key),
|
||||
format!("executions[{index}].executor.key"),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1952,12 +1949,11 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
let target_format = if frame_out {
|
||||
runtime.surface.format
|
||||
} else {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments, ..
|
||||
} = &execution.kind
|
||||
else {
|
||||
if !matches!(execution.kind, ExecutionKind::Fullscreen) {
|
||||
return Err(fail("fullscreen execution is not render"));
|
||||
};
|
||||
}
|
||||
let (color_attachments, _) =
|
||||
crate::render_graph::execution_attachments(execution);
|
||||
let target = color_attachments
|
||||
.first()
|
||||
.ok_or_else(|| fail("fullscreen target missing"))?
|
||||
@@ -2045,21 +2041,17 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
],
|
||||
});
|
||||
executions.push(PreparedExecution::Fullscreen {
|
||||
execution: index,
|
||||
frame_out,
|
||||
bind_group,
|
||||
pipeline,
|
||||
_uniform: uniform,
|
||||
});
|
||||
}
|
||||
"pipeline" => {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
depth_stencil,
|
||||
} = &execution.kind
|
||||
else {
|
||||
_ if contract.is_raster_draw() => {
|
||||
if !matches!(execution.kind, ExecutionKind::RasterDraw) {
|
||||
return Err(fail("pipeline is not render"));
|
||||
};
|
||||
}
|
||||
let (color_attachments, depth_stencil) =
|
||||
crate::render_graph::execution_attachments(execution);
|
||||
let color = color_attachments
|
||||
.first()
|
||||
.ok_or_else(|| fail("pipeline color missing"))?;
|
||||
@@ -2098,8 +2090,8 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.ok_or_else(|| fail("depth allocation invalid"))
|
||||
})
|
||||
.transpose()?;
|
||||
let NormalizedParameters::Pipeline {
|
||||
pipeline: _,
|
||||
let NormalizedParameters::Raster {
|
||||
draw_order: _,
|
||||
depth_compare,
|
||||
depth_write_enabled,
|
||||
..
|
||||
@@ -2145,7 +2137,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
)
|
||||
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
||||
executions.push(PreparedExecution::Pipeline {
|
||||
execution: index,
|
||||
base,
|
||||
predicate_ordinal: runtime
|
||||
.instance_traversal
|
||||
|
||||
@@ -4,13 +4,16 @@ use std::{
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
pub const MAX_PROFILE_PASSES: usize = crate::render_graph::MAX_EXECUTIONS;
|
||||
// A frame can contain every logical execution as a singleton physical pass and
|
||||
// one instance-traversal compute pass.
|
||||
pub const MAX_PROFILE_PASSES: usize = crate::render_graph::MAX_EXECUTIONS + 1;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
const SLOT_COUNT: usize = 4;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
const QUERY_COUNT: u32 = (MAX_PROFILE_PASSES * 2) as u32;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
const RESOLVE_SIZE: u64 = QUERY_COUNT as u64 * 8;
|
||||
const USED_RESOLVE_SIZE: u64 = QUERY_COUNT as u64 * 8;
|
||||
const RESOLVE_SIZE: u64 = USED_RESOLVE_SIZE.next_multiple_of(wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SlotState {
|
||||
@@ -396,7 +399,9 @@ mod tests {
|
||||
#[test]
|
||||
fn capacity_is_aligned() {
|
||||
assert_eq!(RESOLVE_SIZE % wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT, 0);
|
||||
assert_eq!(QUERY_COUNT, 2048)
|
||||
assert!(RESOLVE_SIZE >= USED_RESOLVE_SIZE);
|
||||
assert!(RESOLVE_SIZE - USED_RESOLVE_SIZE < wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT);
|
||||
assert_eq!(QUERY_COUNT as usize, MAX_PROFILE_PASSES * 2)
|
||||
}
|
||||
#[test]
|
||||
fn lazy_identity_when_disabled_unavailable_or_full() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { semanticCatalog } from "./catalog.js";
|
||||
import { NODE_TITLE_OVERRIDES, semanticCatalog } from "./catalog.js";
|
||||
|
||||
const GROUPS = Object.freeze([
|
||||
["source", "Source"],
|
||||
@@ -16,7 +16,7 @@ export const addNodeItems = Object.freeze(
|
||||
GROUPS.flatMap(([execution, group]) =>
|
||||
Object.entries(semanticCatalog)
|
||||
.filter(([, definition]) => definition.execution === execution)
|
||||
.map(([typeId]) => Object.freeze({ typeId, title: title(typeId), group })),
|
||||
.map(([typeId]) => Object.freeze({ typeId, title: NODE_TITLE_OVERRIDES[typeId] ?? title(typeId), group })),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const GRAPH_ID = "authored_gpu_culling";
|
||||
export const CATALOG_VERSION = 11;
|
||||
export const CATALOG_VERSION = 12;
|
||||
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,
|
||||
@@ -63,6 +63,20 @@ const texture = {
|
||||
viewFormats: [],
|
||||
},
|
||||
};
|
||||
const rasterInputs = () => ({
|
||||
mesh: i("mesh_data"),
|
||||
predicate: i("bool", 0),
|
||||
colorTarget: i("texture", 0, undefined, "compiler_texture"),
|
||||
depthTarget: i("texture", 0, undefined, "compiler_texture"),
|
||||
});
|
||||
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() });
|
||||
export const NODE_TITLE_OVERRIDES = Object.freeze({
|
||||
ground_plane: "Ground Plane",
|
||||
gltf_standard: "glTF Standard",
|
||||
gltf_standard_double_sided: "glTF Standard — Double-Sided",
|
||||
});
|
||||
export const semanticCatalog = Object.freeze({
|
||||
mesh: {
|
||||
version: 2,
|
||||
@@ -107,18 +121,9 @@ export const semanticCatalog = Object.freeze({
|
||||
outputs: { isFrustumCulled: o("bool") },
|
||||
parameters: { cameraSelection: "active" },
|
||||
},
|
||||
pipeline: {
|
||||
version: 4,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
predicate: i("bool", 0),
|
||||
colorTarget: i("texture", 0, undefined, "compiler_texture"),
|
||||
depthTarget: i("texture", 0, undefined, "compiler_texture"),
|
||||
},
|
||||
outputs: { color: o("texture"), depth: o("texture") },
|
||||
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
|
||||
},
|
||||
ground_plane: raster(),
|
||||
gltf_standard: raster(),
|
||||
gltf_standard_double_sided: raster(),
|
||||
...expressionCatalog,
|
||||
fullscreen_copy: {
|
||||
version: 1,
|
||||
@@ -292,7 +297,6 @@ const enumeration = (value, values) => ({
|
||||
default: tagged("string", value),
|
||||
enum: values,
|
||||
});
|
||||
const string = (value) => ({ type: "string", default: tagged("string", value) });
|
||||
const boolean = (value) => ({
|
||||
type: "boolean",
|
||||
default: tagged("boolean", value),
|
||||
@@ -327,7 +331,7 @@ const zero = (type) => {
|
||||
Array.from({ length: size }, (_, row) => Number(column === row)));
|
||||
};
|
||||
const defaultForInput = (key, name, type) => {
|
||||
if (key === "pipeline" && name === "predicate") return true;
|
||||
if (["ground_plane", "gltf_standard", "gltf_standard_double_sided"].includes(key) && name === "predicate") return true;
|
||||
if (key === "and") return true;
|
||||
if (/^combine_mat[234]$/.test(key)) {
|
||||
const index = Number(name.replace("column", ""));
|
||||
@@ -374,8 +378,8 @@ const parameterSchemas = {
|
||||
},
|
||||
mesh: {},
|
||||
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
||||
pipeline: {
|
||||
pipeline: string("gltf_standard"),
|
||||
ground_plane: {
|
||||
drawOrder: { ...number(0, -2147483648, 2147483647), integer: true },
|
||||
depthCompare: enumeration("less_equal", [
|
||||
"never",
|
||||
"less",
|
||||
@@ -418,6 +422,8 @@ const parameterSchemas = {
|
||||
},
|
||||
};
|
||||
for (const key of Object.keys(expressionCatalog)) parameterSchemas[key] = {};
|
||||
parameterSchemas.gltf_standard = structuredClone(parameterSchemas.ground_plane);
|
||||
parameterSchemas.gltf_standard_double_sided = structuredClone(parameterSchemas.ground_plane);
|
||||
export const nodeDefinitions = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||
const sockets = {
|
||||
@@ -453,7 +459,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
key,
|
||||
{
|
||||
version: c.version,
|
||||
title: key.replaceAll("_", " "),
|
||||
title: NODE_TITLE_OVERRIDES[key] ?? key.replaceAll("_", " "),
|
||||
behavior: "standard",
|
||||
style: c.execution,
|
||||
parameters,
|
||||
@@ -475,6 +481,13 @@ 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";
|
||||
}
|
||||
}
|
||||
nodeDefinitions.color_balance.ui = [
|
||||
{ kind: "parameter", parameter: "mode" },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
|
||||
@@ -31,20 +31,20 @@ const predicates = (withCulling = false) => {
|
||||
result.find((item) => item.id === id).inputs.inputs.push(...input("not_culled", "value"));
|
||||
return { nodes: result, classes: { ground: "ground_class", pbr: "standard_class", pbr_double: "double_class" } };
|
||||
};
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false) => {
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false, sampleCount = 1) => {
|
||||
const classification = predicates(withCulling);
|
||||
const explicit = colorTarget || heightScale !== 1;
|
||||
const target = colorTarget || "hdr";
|
||||
return [
|
||||
...(explicit && !colorTarget ? [node("hdr", "texture", texture("rgba16_float", 1, heightScale))] : []),
|
||||
...(!colorTarget ? [node("hdr", "texture", texture("rgba16_float", 1, heightScale, sampleCount))] : []),
|
||||
node("scene_depth", "texture", texture("depth32_float", 1, heightScale, sampleCount)),
|
||||
node("mesh", "mesh"),
|
||||
...classification.nodes,
|
||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), ...(explicit ? { colorTarget: input(target, "texture") } : {}) }),
|
||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
||||
node("pbr_double", "pipeline", { pipeline: "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"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||
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") }),
|
||||
];
|
||||
};
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 3, graphId, revision: 2, nodes });
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 3, graphId, revision: 3, nodes });
|
||||
const direct = (graphId, clearColor) => graph(graphId, [
|
||||
node("ldr", "texture", texture("rgba8_unorm")),
|
||||
...scene("ldr", clearColor),
|
||||
@@ -58,7 +58,7 @@ export const hdr = graph("preset_hdr_fullscreen", [
|
||||
]);
|
||||
export const msaa = graph("preset_msaa", [
|
||||
node("msaa_hdr", "texture", texture("rgba16_float", 1, 1, 4)),
|
||||
...scene("msaa_hdr"),
|
||||
...scene("msaa_hdr", undefined, 1, false, 4),
|
||||
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
||||
]);
|
||||
export const culling = graph("preset_gpu_culling", (() => {
|
||||
|
||||
@@ -11,12 +11,14 @@ import {
|
||||
} from "../static/render-graph/node-spawn.js";
|
||||
|
||||
test("add-node model contains all final catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 42);
|
||||
assert.equal(addNodeItems.length, 44);
|
||||
assert.deepEqual(
|
||||
[...new Set(addNodeItems.map((item) => item.group))],
|
||||
["Source", "Expression", "Render / post", "Frame"],
|
||||
);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 42);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 44);
|
||||
assert.deepEqual(addNodeItems.filter((item) => ["ground_plane", "gltf_standard", "gltf_standard_double_sided"].includes(item.typeId)).map((item) => item.title), ["Ground Plane", "glTF Standard", "glTF Standard — Double-Sided"]);
|
||||
assert.ok(!addNodeItems.some((item) => item.typeId === "pipeline" || item.title === "Pipeline"));
|
||||
assert.ok(addNodeItems.some((item) => item.typeId === "separate_u32_bits" && item.group === "Expression"));
|
||||
assert.ok(!addNodeItems.some((item) => ["mesh_query", "pipeline_registry"].includes(item.typeId)));
|
||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
||||
|
||||
@@ -36,8 +36,8 @@ 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, 11);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 42);
|
||||
assert.equal(fxNodeComposition.version, 12);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 44);
|
||||
assert.ok(
|
||||
Object.values(fxNodeComposition.nodes).every(
|
||||
(definition) => definition.migrations.length === 0,
|
||||
|
||||
@@ -30,15 +30,18 @@ const authoredLink = (id, from, to = "target", muted = false) => ({
|
||||
toNodeId: to, toSocketId: `${to}:inputs`, muted, extensions: {},
|
||||
});
|
||||
|
||||
test("catalog v11 exposes the final mesh, pipeline, and typed-expression contracts", () => {
|
||||
assert.equal(CATALOG_VERSION, 11);
|
||||
test("catalog v12 exposes raster and typed-expression contracts", () => {
|
||||
assert.equal(CATALOG_VERSION, 12);
|
||||
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
||||
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
||||
});
|
||||
assert.equal(semanticCatalog.mesh.version, 2);
|
||||
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
||||
assert.equal(semanticCatalog.pipeline.version, 4);
|
||||
assert.deepEqual(semanticCatalog.pipeline.inputs.predicate.cardinality, { minimum: 0, maximum: 1 });
|
||||
assert.equal(semanticCatalog.pipeline, undefined);
|
||||
for (const key of ["ground_plane", "gltf_standard", "gltf_standard_double_sided"]) {
|
||||
assert.equal(semanticCatalog[key].version, 1);
|
||||
assert.deepEqual(semanticCatalog[key].inputs.predicate.cardinality, { minimum: 0, maximum: 1 });
|
||||
}
|
||||
assert.deepEqual(semanticCatalog.and.inputs.inputs.cardinality, { minimum: 0, maximum: 8 });
|
||||
assert.equal(nodeDefinitions.and.sockets.inputs.maxIncomingLinks, 8);
|
||||
assert.equal(nodeDefinitions.and.sockets.inputs.value, null);
|
||||
@@ -52,21 +55,34 @@ test("catalog v11 exposes the final mesh, pipeline, and typed-expression contrac
|
||||
}
|
||||
});
|
||||
|
||||
test("raster declarations own their defaults and sockets", () => {
|
||||
const keys = ["ground_plane", "gltf_standard", "gltf_standard_double_sided"];
|
||||
assert.deepEqual(keys.map((key) => nodeDefinitions[key].title), [
|
||||
"Ground Plane", "glTF Standard", "glTF Standard — Double-Sided",
|
||||
]);
|
||||
assert.notStrictEqual(semanticCatalog.ground_plane.inputs, semanticCatalog.gltf_standard.inputs);
|
||||
assert.notStrictEqual(nodeDefinitions.ground_plane.sockets, nodeDefinitions.gltf_standard.sockets);
|
||||
assert.notStrictEqual(nodeDefinitions.ground_plane.parameters, nodeDefinitions.gltf_standard.parameters);
|
||||
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);
|
||||
});
|
||||
|
||||
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, 4);
|
||||
assert.equal(byId.ground.executor.version, 1);
|
||||
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"]) {
|
||||
assert.equal(semanticCatalog.pipeline.inputs[socket].defaultPolicy, "compiler_texture");
|
||||
assert.equal(nodeDefinitions.pipeline.sockets[socket].default, undefined);
|
||||
assert.equal(semanticCatalog.gltf_standard.inputs[socket].defaultPolicy, "compiler_texture");
|
||||
assert.equal(nodeDefinitions.gltf_standard.sockets[socket].default, undefined);
|
||||
}
|
||||
assert.equal(semanticCatalog.pipeline.inputs.predicate.defaultPolicy, "parameter_literal");
|
||||
assert.notEqual(nodeDefinitions.pipeline.sockets.predicate.default, null);
|
||||
assert.equal(semanticCatalog.gltf_standard.inputs.predicate.defaultPolicy, "parameter_literal");
|
||||
assert.notEqual(nodeDefinitions.gltf_standard.sockets.predicate.default, null);
|
||||
});
|
||||
|
||||
test("removed architecture is absent from the authoring catalog", () => {
|
||||
|
||||
@@ -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, 2], name);
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [3, 3], 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)
|
||||
@@ -15,7 +15,7 @@ test("all presets use current schemas, versions, and one frame output", () => {
|
||||
const authoredX4 = Object.entries(presets.renderGraphPresets).flatMap(([name, graph]) =>
|
||||
graph.nodes.filter((node) => node.parameters?.texture?.sampleCount === 4)
|
||||
.map((node) => `${name}:${node.id}`));
|
||||
assert.deepEqual(authoredX4, ["msaa:msaa_hdr"]);
|
||||
assert.deepEqual(authoredX4, ["msaa:msaa_hdr", "msaa:scene_depth"]);
|
||||
assert.equal(typeof presets.msaa.nodes.find((node) => node.id === "msaa_hdr")
|
||||
.parameters.texture.sampleCount, "number");
|
||||
});
|
||||
@@ -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, 4);
|
||||
assert.equal(pipeline.executor.version, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -45,14 +45,20 @@ test("culling adds a local-AABB expression to each material predicate", () => {
|
||||
assert.equal(byId[id].inputs.inputs.at(-1).node, "not_culled");
|
||||
});
|
||||
|
||||
test("implicit presets start disconnected and then chain both attachments", () => {
|
||||
for (const name of ["hdr", "culling", "grading"]) {
|
||||
const byId = Object.fromEntries(presets[name].nodes.map((node) => [node.id, node]));
|
||||
assert.equal("colorTarget" in byId.ground.inputs, false, name);
|
||||
assert.equal("depthTarget" in byId.ground.inputs, false, name);
|
||||
assert.deepEqual(byId.pbr.inputs.colorTarget, [{ node: "ground", socket: "color" }], name);
|
||||
assert.deepEqual(byId.pbr.inputs.depthTarget, [{ node: "ground", socket: "depth" }], name);
|
||||
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;
|
||||
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.equal(["ground", "pbr", "pbr_double"].includes(color[0].node), false, name);
|
||||
}
|
||||
const colorDescriptor = byId[color[0].node].parameters.texture;
|
||||
const depthDescriptor = byId[depth[0].node].parameters.texture;
|
||||
assert.equal(depthDescriptor.format, "depth32_float", name);
|
||||
assert.deepEqual(depthDescriptor.extent, colorDescriptor.extent, name);
|
||||
assert.equal(depthDescriptor.sampleCount, colorDescriptor.sampleCount, name);
|
||||
}
|
||||
const midnight = Object.fromEntries(presets.midnight.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(midnight.ground.inputs.colorTarget, [{ node: "ldr", socket: "texture" }]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user