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)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
struct PipelineParameters {
|
struct RasterParameters {
|
||||||
pipeline: String,
|
draw_order: i32,
|
||||||
depth_compare: CompareFunction,
|
depth_compare: CompareFunction,
|
||||||
depth_write_enabled: bool,
|
depth_write_enabled: bool,
|
||||||
clear_depth: f32,
|
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)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct OutputKey(usize, u16);
|
struct OutputKey(usize, u16);
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
#[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 {
|
enum TransitionTargetKey {
|
||||||
Authored(OutputKey),
|
Authored(OutputKey),
|
||||||
CompilerDefaultInput {
|
CompilerDefaultInput {
|
||||||
@@ -244,6 +249,7 @@ fn reaches(
|
|||||||
from: usize,
|
from: usize,
|
||||||
to: usize,
|
to: usize,
|
||||||
outgoing_edges: &[Vec<usize>],
|
outgoing_edges: &[Vec<usize>],
|
||||||
|
ordering_outgoing: &[Vec<usize>],
|
||||||
edges: &[DependencyEdge],
|
edges: &[DependencyEdge],
|
||||||
live: &HashSet<usize>,
|
live: &HashSet<usize>,
|
||||||
memo: &mut HashMap<(usize, usize), bool>,
|
memo: &mut HashMap<(usize, usize), bool>,
|
||||||
@@ -268,6 +274,11 @@ fn reaches(
|
|||||||
stack.push(next);
|
stack.push(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for &next in &ordering_outgoing[node] {
|
||||||
|
if live.contains(&next) {
|
||||||
|
stack.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
memo.insert((from, to), answer);
|
memo.insert((from, to), answer);
|
||||||
answer
|
answer
|
||||||
@@ -741,21 +752,9 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
descriptor: normalize_texture(p.texture, &base)?,
|
descriptor: normalize_texture(p.texture, &base)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pipeline" => {
|
key if contract(key).is_some_and(|contract| contract.is_raster_draw()) => {
|
||||||
let p: PipelineParameters =
|
let p: RasterParameters =
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
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) {
|
if !p.clear_depth.is_finite() || !(0.0..=1.0).contains(&p.clear_depth) {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_PARAMETERS_INVALID",
|
"GRAPH_PARAMETERS_INVALID",
|
||||||
@@ -770,8 +769,8 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
format!("{base}.clearColor"),
|
format!("{base}.clearColor"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
NormalizedParameters::Pipeline {
|
NormalizedParameters::Raster {
|
||||||
pipeline: p.pipeline,
|
draw_order: p.draw_order,
|
||||||
depth_compare: p.depth_compare,
|
depth_compare: p.depth_compare,
|
||||||
depth_write_enabled: p.depth_write_enabled,
|
depth_write_enabled: p.depth_write_enabled,
|
||||||
clear_depth: p.clear_depth,
|
clear_depth: p.clear_depth,
|
||||||
@@ -1102,6 +1101,271 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
e.producer_output_ordinal,
|
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()];
|
let mut deps = vec![Vec::new(); graph.nodes.len()];
|
||||||
for edge in &edges {
|
for edge in &edges {
|
||||||
deps[edge.to_node].push(edge.from_node);
|
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());
|
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.
|
// IDs are independent of scheduling: original node order, then contract output order.
|
||||||
// Source nodes expose only outputs that survived active-edge/liveness analysis;
|
// Source nodes expose only outputs that survived active-edge/liveness analysis;
|
||||||
// executable nodes retain their complete output shape for runtime lowering.
|
// 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_roots: Vec<DefaultDraft> = Vec::new();
|
||||||
let mut default_targets = HashMap::new();
|
let mut default_targets = HashMap::new();
|
||||||
for i in 0..graph.nodes.len() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
for (input_ordinal, (socket, role, format, opposite)) in [
|
for (socket, role, format, opposite) in [
|
||||||
(
|
(
|
||||||
"colorTarget",
|
"colorTarget",
|
||||||
CompilerTextureRole::ColorTarget,
|
CompilerTextureRole::ColorTarget,
|
||||||
@@ -1233,12 +1533,16 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
|
||||||
{
|
{
|
||||||
if bound[i].contains_key(socket) {
|
if bound[i].contains_key(socket) {
|
||||||
continue;
|
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 {
|
let key = TransitionTargetKey::CompilerDefaultInput {
|
||||||
owner_node: i,
|
owner_node: i,
|
||||||
input_ordinal,
|
input_ordinal,
|
||||||
@@ -1267,8 +1571,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if !live.contains(&i) {
|
if !live.contains(&i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
|
let transition_sockets: &[(&str, u16)] = match contracts[i] {
|
||||||
"pipeline" => &[("colorTarget", 0), ("depthTarget", 1)],
|
ref contract if contract.is_raster_draw() => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||||
_ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)],
|
_ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)],
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
@@ -1388,6 +1692,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
outgoing_edges[edge.from_node].push(index);
|
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 {
|
for outgoing in &mut outgoing_edges {
|
||||||
outgoing.sort_by_key(|&index| {
|
outgoing.sort_by_key(|&index| {
|
||||||
let edge = &edges[index];
|
let edge = &edges[index];
|
||||||
@@ -1404,7 +1714,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if !live.contains(&i) {
|
if !live.contains(&i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if contracts[i].key == "pipeline" {
|
if contracts[i].is_raster_draw() {
|
||||||
if bound[i].get("colorTarget").map(|b| b.producer)
|
if bound[i].get("colorTarget").map(|b| b.producer)
|
||||||
== bound[i].get("depthTarget").map(|b| b.producer)
|
== bound[i].get("depthTarget").map(|b| b.producer)
|
||||||
&& bound[i].contains_key("colorTarget")
|
&& 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),
|
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(
|
return Err(error(
|
||||||
"GRAPH_ILLEGAL_ACCESS",
|
"GRAPH_ILLEGAL_ACCESS",
|
||||||
"multisampled color texture is not a produced pipeline color",
|
"multisampled color texture is not a produced pipeline color",
|
||||||
@@ -1571,6 +1881,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
i,
|
i,
|
||||||
next.writer_node,
|
next.writer_node,
|
||||||
&outgoing_edges,
|
&outgoing_edges,
|
||||||
|
&ordering_outgoing,
|
||||||
&edges,
|
&edges,
|
||||||
&live,
|
&live,
|
||||||
&mut reachability,
|
&mut reachability,
|
||||||
@@ -1587,7 +1898,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
|
|
||||||
// Validate every independently resolved attachment before graph cycle reporting.
|
// Validate every independently resolved attachment before graph cycle reporting.
|
||||||
for i in 0..graph.nodes.len() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let cd = version_of
|
let cd = version_of
|
||||||
@@ -1780,12 +2091,23 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stable Kahn scheduling is deliberately after resource and access validation.
|
// 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()];
|
let mut indegree = vec![0; graph.nodes.len()];
|
||||||
for &node in &live {
|
for &node in &live {
|
||||||
indegree[node] = edges
|
indegree[node] = edges
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|edge| edge.to_node == node && live.contains(&edge.from_node))
|
.filter(|edge| edge.to_node == node && live.contains(&edge.from_node))
|
||||||
.count();
|
.count()
|
||||||
|
+ ordering_edges
|
||||||
|
.iter()
|
||||||
|
.filter(|(from, to)| *to == node && live.contains(from))
|
||||||
|
.count();
|
||||||
}
|
}
|
||||||
let mut queue = BinaryHeap::new();
|
let mut queue = BinaryHeap::new();
|
||||||
for &node in &live {
|
for &node in &live {
|
||||||
@@ -1803,6 +2125,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
queue.push(Reverse(consumer));
|
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() {
|
if order.len() != live.len() {
|
||||||
let residual: Vec<_> = (0..graph.nodes.len())
|
let residual: Vec<_> = (0..graph.nodes.len())
|
||||||
@@ -1811,6 +2139,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
fn cycle_dfs(
|
fn cycle_dfs(
|
||||||
node: usize,
|
node: usize,
|
||||||
outgoing_edges: &[Vec<usize>],
|
outgoing_edges: &[Vec<usize>],
|
||||||
|
ordering_outgoing: &[Vec<usize>],
|
||||||
edges: &[DependencyEdge],
|
edges: &[DependencyEdge],
|
||||||
residual: &[bool],
|
residual: &[bool],
|
||||||
colors: &mut [u8],
|
colors: &mut [u8],
|
||||||
@@ -1829,6 +2158,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if let Some(cycle) = cycle_dfs(
|
if let Some(cycle) = cycle_dfs(
|
||||||
to,
|
to,
|
||||||
outgoing_edges,
|
outgoing_edges,
|
||||||
|
ordering_outgoing,
|
||||||
edges,
|
edges,
|
||||||
residual,
|
residual,
|
||||||
colors,
|
colors,
|
||||||
@@ -1848,6 +2178,31 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
return Some(cycle);
|
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();
|
node_stack.pop();
|
||||||
colors[node] = 2;
|
colors[node] = 2;
|
||||||
None
|
None
|
||||||
@@ -1859,6 +2214,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
cycle = cycle_dfs(
|
cycle = cycle_dfs(
|
||||||
node,
|
node,
|
||||||
&outgoing_edges,
|
&outgoing_edges,
|
||||||
|
&ordering_outgoing,
|
||||||
&edges,
|
&edges,
|
||||||
&residual,
|
&residual,
|
||||||
&mut colors,
|
&mut colors,
|
||||||
@@ -2189,15 +2545,15 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
.collect();
|
.collect();
|
||||||
let mut accesses = Vec::new();
|
let mut accesses = Vec::new();
|
||||||
let kind = match contracts[i].key {
|
let kind = match contracts[i].key {
|
||||||
"pipeline" => {
|
_ if contracts[i].is_raster_draw() => {
|
||||||
let color = output_ids[&OutputKey(i, 0)];
|
let color = output_ids[&OutputKey(i, 0)];
|
||||||
let depth = output_ids[&OutputKey(i, 1)];
|
let depth = output_ids[&OutputKey(i, 1)];
|
||||||
let clear = match params[i] {
|
let clear = match params[i] {
|
||||||
NormalizedParameters::Pipeline { clear_color, .. } => clear_color,
|
NormalizedParameters::Raster { clear_color, .. } => clear_color,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let clear_depth = match ¶ms[i] {
|
let clear_depth = match ¶ms[i] {
|
||||||
NormalizedParameters::Pipeline { clear_depth, .. } => *clear_depth,
|
NormalizedParameters::Raster { clear_depth, .. } => *clear_depth,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
|
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
|
||||||
@@ -2280,20 +2636,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ExecutionKind::Render {
|
ExecutionKind::RasterDraw
|
||||||
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,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ if contracts[i].fullscreen_policy.is_some() => {
|
_ if contracts[i].fullscreen_policy.is_some() => {
|
||||||
let color = output_ids[&OutputKey(i, 0)];
|
let color = output_ids[&OutputKey(i, 0)];
|
||||||
@@ -2321,16 +2664,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
full_overwrite: true,
|
full_overwrite: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ExecutionKind::Render {
|
ExecutionKind::Fullscreen
|
||||||
color_attachments: vec![ColorAttachmentPlan {
|
|
||||||
resource: color,
|
|
||||||
resolve_target: None,
|
|
||||||
location: 0,
|
|
||||||
load,
|
|
||||||
store: StoreOp::Store,
|
|
||||||
}],
|
|
||||||
depth_stencil: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"frame_out" => {
|
"frame_out" => {
|
||||||
let r = input_resource("color");
|
let r = input_resource("color");
|
||||||
@@ -2584,7 +2918,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
let mut predicates = Vec::new();
|
let mut predicates = Vec::new();
|
||||||
for (execution, compiled) in executions.iter().enumerate() {
|
for (execution, compiled) in executions.iter().enumerate() {
|
||||||
let node = compiled.original_node_index as usize;
|
let node = compiled.original_node_index as usize;
|
||||||
if contracts[node].key != "pipeline" {
|
if !contracts[node].is_raster_draw() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mesh = output_ids[&bound[node]["mesh"].producer];
|
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") {
|
let predicate = if let Some(binding) = bound[node].get("predicate") {
|
||||||
expression_ids[&binding.producer]
|
expression_ids[&binding.producer]
|
||||||
} else {
|
} else {
|
||||||
let NormalizedParameters::Pipeline {
|
let NormalizedParameters::Raster {
|
||||||
predicate_default, ..
|
predicate_default, ..
|
||||||
} = params[node]
|
} = params[node]
|
||||||
else {
|
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() {
|
for (ordinal, e) in executions.iter().enumerate() {
|
||||||
let ordinal = ordinal as u32;
|
let ordinal = execution_pass[ordinal];
|
||||||
let mut touched = BTreeSet::new();
|
let mut touched = BTreeSet::new();
|
||||||
for x in &e.inputs {
|
for x in &e.inputs {
|
||||||
touched.insert(x.resource);
|
touched.insert(x.resource);
|
||||||
@@ -2710,6 +3050,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
node_count: graph.nodes.len() as u32,
|
node_count: graph.nodes.len() as u32,
|
||||||
resources,
|
resources,
|
||||||
executions,
|
executions,
|
||||||
|
render_passes,
|
||||||
texture_families: families,
|
texture_families: families,
|
||||||
allocation_classes: classes,
|
allocation_classes: classes,
|
||||||
culled_node_count: (graph.nodes.len() - live.len()) as u32,
|
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 {
|
fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
|
||||||
match e {
|
match e {
|
||||||
NormalizedTextureExtent::Absolute {
|
NormalizedTextureExtent::Absolute {
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ pub struct Contract {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub fullscreen_policy: Option<FullscreenPolicy>,
|
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::*;
|
use SemanticType::*;
|
||||||
const R: InputCardinality = InputCardinality { min: 1, max: 1 };
|
const R: InputCardinality = InputCardinality { min: 1, max: 1 };
|
||||||
@@ -138,7 +143,7 @@ const MESH_O: &[OutputSocketContract] = &[
|
|||||||
o("localAabb", LocalAabb),
|
o("localAabb", LocalAabb),
|
||||||
];
|
];
|
||||||
const TEXTURE_O: &[OutputSocketContract] = &[o("texture", Texture)];
|
const TEXTURE_O: &[OutputSocketContract] = &[o("texture", Texture)];
|
||||||
const PIPE_I: &[InputSocketContract] = &[
|
const RASTER_I: &[InputSocketContract] = &[
|
||||||
i("mesh", MeshData, R, InputRole::SemanticRead),
|
i("mesh", MeshData, R, InputRole::SemanticRead),
|
||||||
i("predicate", Bool, O, InputRole::Expression),
|
i("predicate", Bool, O, InputRole::Expression),
|
||||||
i(
|
i(
|
||||||
@@ -149,7 +154,7 @@ const PIPE_I: &[InputSocketContract] = &[
|
|||||||
),
|
),
|
||||||
i("depthTarget", Texture, O, InputRole::DepthTarget),
|
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] = &[
|
const CULL_I: &[InputSocketContract] = &[
|
||||||
i("mesh", MeshData, R, InputRole::Expression),
|
i("mesh", MeshData, R, InputRole::Expression),
|
||||||
i("localAabb", LocalAabb, 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!("mesh", 2, Source, NONE_I, MESH_O, false, None),
|
||||||
c!("texture", 2, Source, NONE_I, TEXTURE_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!("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!(
|
c!(
|
||||||
"and",
|
"and",
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Device-free render graph compiler and compiled graph registry.
|
//! Device-free render graph compiler and compiled graph registry.
|
||||||
|
|
||||||
mod compiler;
|
mod compiler;
|
||||||
|
pub(crate) use compiler::execution_attachments;
|
||||||
mod contracts;
|
mod contracts;
|
||||||
mod expression;
|
mod expression;
|
||||||
mod plan;
|
mod plan;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub struct CompiledGraph {
|
|||||||
pub node_count: u32,
|
pub node_count: u32,
|
||||||
pub resources: Vec<CompiledResource>,
|
pub resources: Vec<CompiledResource>,
|
||||||
pub executions: Vec<CompiledExecution>,
|
pub executions: Vec<CompiledExecution>,
|
||||||
|
pub render_passes: Vec<PhysicalRenderPass>,
|
||||||
pub texture_families: Vec<TextureFamily>,
|
pub texture_families: Vec<TextureFamily>,
|
||||||
pub allocation_classes: Vec<AllocationClass>,
|
pub allocation_classes: Vec<AllocationClass>,
|
||||||
pub culled_node_count: u32,
|
pub culled_node_count: u32,
|
||||||
@@ -108,16 +109,29 @@ pub struct CompiledSocketOutput {
|
|||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum ExecutionKind {
|
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>,
|
color_attachments: Vec<ColorAttachmentPlan>,
|
||||||
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
||||||
},
|
},
|
||||||
FrameOut {
|
Surface,
|
||||||
color: u32,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ColorAttachmentPlan {
|
pub struct ColorAttachmentPlan {
|
||||||
pub resource: u32,
|
pub resource: u32,
|
||||||
@@ -127,7 +141,7 @@ pub struct ColorAttachmentPlan {
|
|||||||
pub store: StoreOp,
|
pub store: StoreOp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DepthStencilAttachmentPlan {
|
pub struct DepthStencilAttachmentPlan {
|
||||||
pub resource: u32,
|
pub resource: u32,
|
||||||
@@ -206,8 +220,8 @@ pub enum NormalizedParameters {
|
|||||||
ExpressionDefaults {
|
ExpressionDefaults {
|
||||||
defaults: Vec<TypedLiteral>,
|
defaults: Vec<TypedLiteral>,
|
||||||
},
|
},
|
||||||
Pipeline {
|
Raster {
|
||||||
pipeline: String,
|
draw_order: i32,
|
||||||
depth_compare: CompareFunction,
|
depth_compare: CompareFunction,
|
||||||
depth_write_enabled: bool,
|
depth_write_enabled: bool,
|
||||||
clear_depth: f32,
|
clear_depth: f32,
|
||||||
@@ -472,6 +486,6 @@ pub enum TextureUsage {
|
|||||||
|
|
||||||
impl CompiledGraph {
|
impl CompiledGraph {
|
||||||
pub fn summary(&self, id: [u32; 2]) -> serde_json::Value {
|
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 struct RuntimePlan {
|
||||||
pub allocations: RuntimeAllocationPlan,
|
pub allocations: RuntimeAllocationPlan,
|
||||||
pub executions: Vec<RuntimeExecution>,
|
pub executions: Vec<RuntimeExecution>,
|
||||||
|
pub render_passes: Vec<PhysicalRenderPass>,
|
||||||
pub instance_traversal: Option<InstanceTraversalPlan>,
|
pub instance_traversal: Option<InstanceTraversalPlan>,
|
||||||
pub surface: RuntimeSurfaceContract,
|
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)
|
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 {
|
fn execution_supported(key: &str) -> bool {
|
||||||
contract(key).is_some_and(|contract| {
|
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" {
|
if output.socket != "color" {
|
||||||
return Err(invalid("fullscreen outputs mismatch", path("outputs")));
|
return Err(invalid("fullscreen outputs mismatch", path("outputs")));
|
||||||
}
|
}
|
||||||
let ExecutionKind::Render {
|
if !matches!(execution.kind, ExecutionKind::Fullscreen) {
|
||||||
color_attachments,
|
|
||||||
depth_stencil: None,
|
|
||||||
} = &execution.kind
|
|
||||||
else {
|
|
||||||
return Err(invalid("fullscreen render kind mismatch", path("kind")));
|
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 {
|
let [attachment] = color_attachments.as_slice() else {
|
||||||
return Err(invalid("fullscreen attachment mismatch", path("kind")));
|
return Err(invalid("fullscreen attachment mismatch", path("kind")));
|
||||||
};
|
};
|
||||||
@@ -749,7 +742,7 @@ fn validate_pipeline_resolve(
|
|||||||
.filter(|access| matches!(access.mode, AccessMode::ColorResolve { .. }))
|
.filter(|access| matches!(access.mode, AccessMode::ColorResolve { .. }))
|
||||||
.count()
|
.count()
|
||||||
== 1;
|
== 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
|
|| producer.original_node_index != *producer_node_index
|
||||||
|| !exact_output
|
|| !exact_output
|
||||||
|| !matches!(&source.origin,
|
|| !matches!(&source.origin,
|
||||||
@@ -808,6 +801,123 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
"schemaVersion",
|
"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 producers = vec![None; graph.resources.len()];
|
||||||
let mut uses = vec![BTreeSet::new(); 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}]"),
|
format!("executions[{i}]"),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.insert(i as u32);
|
.insert(execution_pass[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (ri, resource) in graph.resources.iter().enumerate() {
|
for (ri, resource) in graph.resources.iter().enumerate() {
|
||||||
@@ -910,6 +1020,52 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
format!("executions[{i}].inputs"),
|
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()
|
.count()
|
||||||
== 1
|
== 1
|
||||||
})
|
})
|
||||||
&& owner_executions[0].executor.key == "pipeline"
|
&& contract(&owner_executions[0].executor.key)
|
||||||
&& owner_executions[0].executor.version == 4
|
.is_some_and(Contract::is_raster_draw)
|
||||||
|
&& owner_executions[0].executor.version == 1
|
||||||
&& graph
|
&& graph
|
||||||
.executions
|
.executions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1013,7 +1170,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
.filter(|input| input.resource == source)
|
.filter(|input| input.resource == source)
|
||||||
.count()
|
.count()
|
||||||
== 1
|
== 1
|
||||||
&& contract("pipeline")
|
&& contract(&owner_executions[0].executor.key)
|
||||||
.and_then(|contract| contract.inputs.get(*input_ordinal as usize))
|
.and_then(|contract| contract.inputs.get(*input_ordinal as usize))
|
||||||
.is_some_and(|input| {
|
.is_some_and(|input| {
|
||||||
input.name == *socket
|
input.name == *socket
|
||||||
@@ -1242,7 +1399,11 @@ fn validate_instance_traversal(graph: &CompiledGraph) -> Result<(), GraphError>
|
|||||||
.executions
|
.executions
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.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();
|
.collect();
|
||||||
let Some(plan) = &graph.instance_traversal else {
|
let Some(plan) = &graph.instance_traversal else {
|
||||||
return if pipeline_indices.is_empty() {
|
return if pipeline_indices.is_empty() {
|
||||||
@@ -1552,7 +1713,7 @@ pub fn prepare_runtime_plan(
|
|||||||
for (i, execution) in graph.executions.iter().enumerate() {
|
for (i, execution) in graph.executions.iter().enumerate() {
|
||||||
let path = format!("executions[{i}]");
|
let path = format!("executions[{i}]");
|
||||||
match execution.executor.key.as_str() {
|
match execution.executor.key.as_str() {
|
||||||
"pipeline" => {}
|
key if contract(key).is_some_and(Contract::is_raster_draw) => {}
|
||||||
_ if contract(&execution.executor.key)
|
_ if contract(&execution.executor.key)
|
||||||
.is_some_and(|contract| contract.fullscreen_policy.is_some()) => {}
|
.is_some_and(|contract| contract.fullscreen_policy.is_some()) => {}
|
||||||
"frame_out" => {
|
"frame_out" => {
|
||||||
@@ -1587,11 +1748,11 @@ pub fn prepare_runtime_plan(
|
|||||||
validate_instance_traversal(graph)?;
|
validate_instance_traversal(graph)?;
|
||||||
|
|
||||||
for (i, execution) in graph.executions.iter().enumerate() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let NormalizedParameters::Pipeline {
|
let NormalizedParameters::Raster {
|
||||||
pipeline,
|
draw_order: _,
|
||||||
clear_depth,
|
clear_depth,
|
||||||
clear_color,
|
clear_color,
|
||||||
..
|
..
|
||||||
@@ -1602,8 +1763,7 @@ pub fn prepare_runtime_plan(
|
|||||||
format!("executions[{i}].parameters"),
|
format!("executions[{i}].parameters"),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
if !valid_pipeline_name(pipeline)
|
if !clear_depth.is_finite()
|
||||||
|| !clear_depth.is_finite()
|
|
||||||
|| !(0.0..=1.0).contains(clear_depth)
|
|| !(0.0..=1.0).contains(clear_depth)
|
||||||
|| clear_color.iter().any(|value| !value.is_finite())
|
|| clear_color.iter().any(|value| !value.is_finite())
|
||||||
{
|
{
|
||||||
@@ -1612,15 +1772,18 @@ pub fn prepare_runtime_plan(
|
|||||||
format!("executions[{i}].parameters"),
|
format!("executions[{i}].parameters"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let ExecutionKind::Render {
|
if !matches!(execution.kind, ExecutionKind::RasterDraw) {
|
||||||
color_attachments,
|
|
||||||
depth_stencil: Some(depth_attachment),
|
|
||||||
} = &execution.kind
|
|
||||||
else {
|
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"pipeline render kind mismatch",
|
"pipeline render kind mismatch",
|
||||||
format!("executions[{i}].kind"),
|
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 {
|
let [color_attachment] = color_attachments.as_slice() else {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
@@ -2307,6 +2470,7 @@ pub fn prepare_runtime_plan(
|
|||||||
resource_allocations,
|
resource_allocations,
|
||||||
},
|
},
|
||||||
executions,
|
executions,
|
||||||
|
render_passes: graph.render_passes.clone(),
|
||||||
instance_traversal: graph.instance_traversal.clone(),
|
instance_traversal: graph.instance_traversal.clone(),
|
||||||
surface,
|
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("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
|
||||||
node("class", "and", 2, json!({}),
|
node("class", "and", 2, json!({}),
|
||||||
json!({"inputs":[input("bits","bit0")[0].clone(),input("visible","value")[0].clone()]})),
|
json!({"inputs":[input("bits","bit0")[0].clone(),input("visible","value")[0].clone()]})),
|
||||||
node("pipeline", "pipeline", 4,
|
node("pipeline", "gltf_standard", 1,
|
||||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
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")})),
|
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")})),
|
||||||
node("frame", "frame_out", 3,
|
node("frame", "frame_out", 3,
|
||||||
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,
|
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,
|
||||||
@@ -62,7 +62,16 @@ pub(crate) fn full_cull_graph() -> Value {
|
|||||||
#[test]
|
#[test]
|
||||||
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
||||||
assert_eq!(contract("mesh").unwrap().version, 2);
|
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!(
|
assert_eq!(
|
||||||
contract("mesh")
|
contract("mesh")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -76,7 +85,7 @@ fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
|||||||
("localAabb", SemanticType::LocalAabb)
|
("localAabb", SemanticType::LocalAabb)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let predicate = contract("pipeline")
|
let predicate = contract("gltf_standard")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -348,7 +357,7 @@ fn pipeline_predicate_defaults_true_and_expression_edges_are_validated() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
pipeline.parameters,
|
pipeline.parameters,
|
||||||
NormalizedParameters::Pipeline {
|
NormalizedParameters::Raster {
|
||||||
predicate_default: true,
|
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");
|
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]
|
#[test]
|
||||||
fn expression_provenance_rejects_cross_mesh_values() {
|
fn expression_provenance_rejects_cross_mesh_values() {
|
||||||
let mut graph = full_cull_graph();
|
let mut graph = full_cull_graph();
|
||||||
@@ -375,11 +471,11 @@ fn expression_provenance_rejects_cross_mesh_values() {
|
|||||||
fn implicit_pipeline_graph() -> Value {
|
fn implicit_pipeline_graph() -> Value {
|
||||||
json!({ "schemaVersion": 3, "graphId": "implicit", "revision": 1, "nodes": [
|
json!({ "schemaVersion": 3, "graphId": "implicit", "revision": 1, "nodes": [
|
||||||
node("mesh", "mesh", 2, json!({}), json!({})),
|
node("mesh", "mesh", 2, json!({}), json!({})),
|
||||||
node("first", "pipeline", 4,
|
node("first", "ground_plane", 1,
|
||||||
json!({"pipeline":"ground_plane","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||||
json!({"mesh":input("mesh","mesh")})),
|
json!({"mesh":input("mesh","mesh")})),
|
||||||
node("second", "pipeline", 4,
|
node("second", "gltf_standard", 1,
|
||||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
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")})),
|
json!({"mesh":input("mesh","mesh"),"colorTarget":input("first","color"),"depthTarget":input("first","depth")})),
|
||||||
node("frame", "frame_out", 3,
|
node("frame", "frame_out", 3,
|
||||||
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]}),
|
json!({"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]
|
#[test]
|
||||||
fn contract_v4_declares_strict_default_policies() {
|
fn contract_v4_declares_strict_default_policies() {
|
||||||
let pipeline = contract("pipeline").unwrap();
|
let pipeline = contract("gltf_standard").unwrap();
|
||||||
assert_eq!(pipeline.version, 4);
|
assert_eq!(pipeline.version, 1);
|
||||||
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
|
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pipeline.inputs[1].default_policy,
|
pipeline.inputs[1].default_policy,
|
||||||
@@ -445,13 +825,9 @@ fn disconnected_targets_have_tagged_roots_and_clear_version_zero() {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|e| e.id == "first")
|
.find(|e| e.id == "first")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let ExecutionKind::Render {
|
assert!(matches!(first.kind, ExecutionKind::RasterDraw));
|
||||||
color_attachments,
|
let (color_attachments, depth_stencil) = execution_attachments(first);
|
||||||
depth_stencil: Some(depth),
|
let depth = depth_stencil.unwrap();
|
||||||
} = &first.kind
|
|
||||||
else {
|
|
||||||
panic!()
|
|
||||||
};
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
color_attachments[0].load,
|
color_attachments[0].load,
|
||||||
NormalizedColorLoad::Clear { .. }
|
NormalizedColorLoad::Clear { .. }
|
||||||
@@ -468,13 +844,9 @@ fn implicit_chain_is_deterministic_and_loads_successors() {
|
|||||||
serde_json::to_value(&b).unwrap()
|
serde_json::to_value(&b).unwrap()
|
||||||
);
|
);
|
||||||
let second = a.executions.iter().find(|e| e.id == "second").unwrap();
|
let second = a.executions.iter().find(|e| e.id == "second").unwrap();
|
||||||
let ExecutionKind::Render {
|
assert!(matches!(second.kind, ExecutionKind::RasterDraw));
|
||||||
color_attachments,
|
let (color_attachments, depth_stencil) = execution_attachments(second);
|
||||||
depth_stencil: Some(depth),
|
let depth = depth_stencil.unwrap();
|
||||||
} = &second.kind
|
|
||||||
else {
|
|
||||||
panic!()
|
|
||||||
};
|
|
||||||
assert_eq!(color_attachments[0].load, NormalizedColorLoad::Load);
|
assert_eq!(color_attachments[0].load, NormalizedColorLoad::Load);
|
||||||
assert_eq!(depth.load, NormalizedDepthLoad::Load);
|
assert_eq!(depth.load, NormalizedDepthLoad::Load);
|
||||||
}
|
}
|
||||||
@@ -586,7 +958,8 @@ fn descriptor_dependency_cycle_keeps_graph_cycle_priority() {
|
|||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.remove("depthTarget");
|
.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]
|
#[test]
|
||||||
@@ -595,8 +968,8 @@ fn known_attachment_error_precedes_cycle() {
|
|||||||
graph["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
graph["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
||||||
graph["nodes"][8]["inputs"]["depthTarget"] = input("pipeline", "depth");
|
graph["nodes"][8]["inputs"]["depthTarget"] = input("pipeline", "depth");
|
||||||
let error = compile_value(graph).unwrap_err();
|
let error = compile_value(graph).unwrap_err();
|
||||||
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
assert_eq!(error.code, "GRAPH_ATTACHMENT_LINEAGE_INVALID");
|
||||||
assert_eq!(error.details["path"], "nodes[8].inputs.colorTarget");
|
assert_eq!(error.details["path"], "nodes[8].inputs");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn self_targeting_copy(source_format: &str) -> Value {
|
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]
|
#[test]
|
||||||
fn known_invalid_fullscreen_target_precedes_source_cycle() {
|
fn known_invalid_fullscreen_target_precedes_source_cycle() {
|
||||||
let mut graph = implicit_pipeline_graph();
|
let mut graph = implicit_pipeline_graph();
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
indirect_commands: &wgpu::Buffer,
|
indirect_commands: &wgpu::Buffer,
|
||||||
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||||
) -> Result<(), &'static str> {
|
) -> 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 view = |resource: u32| -> Result<&wgpu::TextureView, &'static str> {
|
||||||
let a = active
|
let a = active
|
||||||
.runtime
|
.runtime
|
||||||
@@ -71,176 +71,163 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
.map(|s| &s.view)
|
.map(|s| &s.view)
|
||||||
.ok_or(" allocation out of bounds")
|
.ok_or(" allocation out of bounds")
|
||||||
};
|
};
|
||||||
for prepared in &active.executions {
|
for physical in &active.runtime.render_passes {
|
||||||
match prepared {
|
let first = *physical.executions.first().ok_or("empty physical pass")? as usize;
|
||||||
PreparedExecution::Fullscreen {
|
let last = *physical.executions.last().ok_or("empty physical pass")? as usize;
|
||||||
execution,
|
let label = if first == last {
|
||||||
frame_out,
|
active
|
||||||
bind_group,
|
.graph
|
||||||
pipeline,
|
.executions
|
||||||
..
|
.get(first)
|
||||||
} => {
|
.ok_or("execution out of bounds")?
|
||||||
let execution = active
|
.id
|
||||||
|
.clone()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}..{}",
|
||||||
|
active
|
||||||
.graph
|
.graph
|
||||||
.executions
|
.executions
|
||||||
.get(*execution)
|
.get(first)
|
||||||
.ok_or(" execution out of bounds")?;
|
.ok_or("execution out of bounds")?
|
||||||
let (target, operations) = if *frame_out {
|
.id,
|
||||||
let ExecutionKind::FrameOut { .. } = execution.kind else {
|
active
|
||||||
return Err("frame_out kind mismatch");
|
.graph
|
||||||
};
|
.executions
|
||||||
(
|
.get(last)
|
||||||
surface,
|
.ok_or("execution out of bounds")?
|
||||||
wgpu::Operations {
|
.id
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
)
|
||||||
store: wgpu::StoreOp::Store,
|
};
|
||||||
},
|
let (colors, depth) = match &physical.kind {
|
||||||
)
|
crate::render_graph::PhysicalRenderPassKind::Surface => (
|
||||||
} else {
|
vec![Some(wgpu::RenderPassColorAttachment {
|
||||||
let ExecutionKind::Render {
|
view: surface,
|
||||||
color_attachments, ..
|
depth_slice: None,
|
||||||
} = &execution.kind
|
resolve_target: None,
|
||||||
else {
|
ops: wgpu::Operations {
|
||||||
return Err("fullscreen is not render");
|
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||||
};
|
store: wgpu::StoreOp::Store,
|
||||||
let color = color_attachments
|
},
|
||||||
.first()
|
})],
|
||||||
.ok_or("fullscreen target missing")?;
|
None,
|
||||||
(
|
),
|
||||||
view(color.resource)?,
|
crate::render_graph::PhysicalRenderPassKind::Texture {
|
||||||
wgpu::Operations {
|
color_attachments,
|
||||||
load: match color.load {
|
depth_stencil,
|
||||||
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
|
} => {
|
||||||
NormalizedColorLoad::Clear { value } => {
|
let colors = color_attachments
|
||||||
wgpu::LoadOp::Clear(wgpu::Color {
|
.iter()
|
||||||
r: value[0],
|
.map(|color| {
|
||||||
g: value[1],
|
Ok(Some(wgpu::RenderPassColorAttachment {
|
||||||
b: value[2],
|
view: view(color.resource)?,
|
||||||
a: value[3],
|
depth_slice: None,
|
||||||
})
|
resolve_target: color.resolve_target.map(view).transpose()?,
|
||||||
}
|
ops: 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
|
||||||
|
},
|
||||||
},
|
},
|
||||||
store: if color.store == StoreOp::Store {
|
}))
|
||||||
wgpu::StoreOp::Store
|
})
|
||||||
} else {
|
.collect::<Result<Vec<_>, &'static str>>()?;
|
||||||
wgpu::StoreOp::Discard
|
let depth = depth_stencil
|
||||||
},
|
.as_ref()
|
||||||
},
|
.map(|depth| -> Result<_, &'static str> {
|
||||||
)
|
Ok(wgpu::RenderPassDepthStencilAttachment {
|
||||||
};
|
view: view(depth.resource)?,
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
depth_ops: Some(wgpu::Operations {
|
||||||
label: Some(&execution.id),
|
load: match depth.load {
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
NormalizedDepthLoad::Load => wgpu::LoadOp::Load,
|
||||||
view: target,
|
NormalizedDepthLoad::Clear { value } => {
|
||||||
depth_slice: None,
|
wgpu::LoadOp::Clear(value)
|
||||||
resolve_target: None,
|
}
|
||||||
ops: operations,
|
},
|
||||||
})],
|
store: if depth.store == StoreOp::Store {
|
||||||
depth_stencil_attachment: None,
|
wgpu::StoreOp::Store
|
||||||
occlusion_query_set: None,
|
} else {
|
||||||
timestamp_writes: profile
|
wgpu::StoreOp::Discard
|
||||||
.as_deref_mut()
|
},
|
||||||
.and_then(|p| p.render_writes(&execution.id)),
|
}),
|
||||||
});
|
stencil_ops: None,
|
||||||
pass.set_pipeline(pipeline);
|
})
|
||||||
pass.set_bind_group(0, bind_group, &[]);
|
})
|
||||||
pass.draw(0..3, 0..1);
|
.transpose()?;
|
||||||
|
(colors, depth)
|
||||||
}
|
}
|
||||||
PreparedExecution::Pipeline {
|
};
|
||||||
execution,
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
base,
|
label: Some(&label),
|
||||||
predicate_ordinal,
|
color_attachments: &colors,
|
||||||
variant,
|
depth_stencil_attachment: depth,
|
||||||
} => {
|
occlusion_query_set: None,
|
||||||
let execution = active
|
timestamp_writes: profile.as_deref_mut().and_then(|p| p.render_writes(&label)),
|
||||||
.graph
|
});
|
||||||
.executions
|
for &member in &physical.executions {
|
||||||
.get(*execution)
|
match active
|
||||||
.ok_or(" execution out of bounds")?;
|
.executions
|
||||||
let ExecutionKind::Render {
|
.get(member as usize)
|
||||||
color_attachments,
|
.ok_or("prepared execution out of bounds")?
|
||||||
depth_stencil,
|
{
|
||||||
} = &execution.kind
|
PreparedExecution::Fullscreen {
|
||||||
else {
|
bind_group,
|
||||||
return Err("pipeline is not render");
|
pipeline,
|
||||||
};
|
..
|
||||||
let color = color_attachments.first().ok_or("pipeline color missing")?;
|
} => {
|
||||||
let depth = depth_stencil.as_ref().ok_or("pipeline depth missing")?;
|
pass.set_pipeline(pipeline);
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
pass.set_bind_group(0, bind_group, &[]);
|
||||||
label: Some(&execution.id),
|
pass.draw(0..3, 0..1);
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
||||||
view: view(color.resource)?,
|
|
||||||
depth_slice: None,
|
|
||||||
resolve_target: color.resolve_target.map(view).transpose()?,
|
|
||||||
ops: 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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})],
|
|
||||||
depth_stencil_attachment: Some(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),
|
|
||||||
},
|
|
||||||
store: if depth.store == StoreOp::Store {
|
|
||||||
wgpu::StoreOp::Store
|
|
||||||
} else {
|
|
||||||
wgpu::StoreOp::Discard
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
stencil_ops: None,
|
|
||||||
}),
|
|
||||||
occlusion_query_set: None,
|
|
||||||
timestamp_writes: profile
|
|
||||||
.as_deref_mut()
|
|
||||||
.and_then(|p| p.render_writes(&execution.id)),
|
|
||||||
});
|
|
||||||
for (i, group) in scene.bind_groups().iter().enumerate() {
|
|
||||||
pass.set_bind_group(i as u32, group, &[]);
|
|
||||||
}
|
}
|
||||||
if let (Some(p), Some(n), Some(u), Some(t), Some(ix), Some(inst)) = (
|
PreparedExecution::Pipeline {
|
||||||
&gpu.positions.buffer,
|
base,
|
||||||
&gpu.normals.buffer,
|
predicate_ordinal,
|
||||||
&gpu.uvs.buffer,
|
variant,
|
||||||
&gpu.tangents.buffer,
|
..
|
||||||
&gpu.indices.buffer,
|
} => {
|
||||||
&gpu.instances.buffer,
|
for (i, group) in scene.bind_groups().iter().enumerate() {
|
||||||
) {
|
pass.set_bind_group(i as u32, group, &[]);
|
||||||
pass.set_vertex_buffer(0, p.slice(..));
|
}
|
||||||
pass.set_vertex_buffer(1, n.slice(..));
|
if let (Some(p), Some(n), Some(u), Some(t), Some(ix), Some(inst)) = (
|
||||||
pass.set_vertex_buffer(2, u.slice(..));
|
&gpu.positions.buffer,
|
||||||
pass.set_vertex_buffer(4, t.slice(..));
|
&gpu.normals.buffer,
|
||||||
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
|
&gpu.uvs.buffer,
|
||||||
pass.set_vertex_buffer(3, inst.slice(..));
|
&gpu.tangents.buffer,
|
||||||
for (draw_index, draw) in gpu.draws.iter().enumerate() {
|
&gpu.indices.buffer,
|
||||||
pass.set_pipeline(variant);
|
&gpu.instances.buffer,
|
||||||
if pipelines.requires_material(*base) {
|
) {
|
||||||
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
pass.set_vertex_buffer(0, p.slice(..));
|
||||||
|
pass.set_vertex_buffer(1, n.slice(..));
|
||||||
|
pass.set_vertex_buffer(2, u.slice(..));
|
||||||
|
pass.set_vertex_buffer(4, t.slice(..));
|
||||||
|
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
pass.set_vertex_buffer(3, inst.slice(..));
|
||||||
|
for (draw_index, draw) in gpu.draws.iter().enumerate() {
|
||||||
|
pass.set_pipeline(variant);
|
||||||
|
if pipelines.requires_material(*base) {
|
||||||
|
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
||||||
|
}
|
||||||
|
pass.draw_indexed_indirect(
|
||||||
|
indirect_commands,
|
||||||
|
crate::renderer::instance_traversal::command_offset(
|
||||||
|
*predicate_ordinal,
|
||||||
|
gpu.draws.len(),
|
||||||
|
draw_index,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
pass.draw_indexed_indirect(
|
|
||||||
indirect_commands,
|
|
||||||
crate::renderer::instance_traversal::command_offset(
|
|
||||||
*predicate_ordinal,
|
|
||||||
gpu.draws.len(),
|
|
||||||
draw_index,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -815,14 +815,11 @@ struct GpuTextureSlot {
|
|||||||
|
|
||||||
enum PreparedExecution {
|
enum PreparedExecution {
|
||||||
Pipeline {
|
Pipeline {
|
||||||
execution: usize,
|
|
||||||
base: crate::render_data::PipelineKey,
|
base: crate::render_data::PipelineKey,
|
||||||
predicate_ordinal: u32,
|
predicate_ordinal: u32,
|
||||||
variant: wgpu::RenderPipeline,
|
variant: wgpu::RenderPipeline,
|
||||||
},
|
},
|
||||||
Fullscreen {
|
Fullscreen {
|
||||||
execution: usize,
|
|
||||||
frame_out: bool,
|
|
||||||
bind_group: wgpu::BindGroup,
|
bind_group: wgpu::BindGroup,
|
||||||
pipeline: wgpu::RenderPipeline,
|
pipeline: wgpu::RenderPipeline,
|
||||||
_uniform: wgpu::Buffer,
|
_uniform: wgpu::Buffer,
|
||||||
@@ -1687,17 +1684,17 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, execution)| {
|
.map(|(index, execution)| {
|
||||||
let NormalizedParameters::Pipeline { pipeline, .. } = &execution.parameters else {
|
let NormalizedParameters::Raster { .. } = &execution.parameters else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
self.resources
|
self.resources
|
||||||
.find_pipeline(pipeline)
|
.find_pipeline(&execution.executor.key)
|
||||||
.map(Some)
|
.map(Some)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
GraphError::at(
|
GraphError::at(
|
||||||
"GRAPH_EXECUTION_UNSUPPORTED",
|
"GRAPH_EXECUTION_UNSUPPORTED",
|
||||||
format!("pipeline '{pipeline}' is not registered"),
|
format!("pipeline '{}' is not registered", execution.executor.key),
|
||||||
format!("executions[{index}].parameters.pipeline"),
|
format!("executions[{index}].executor.key"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1952,12 +1949,11 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
let target_format = if frame_out {
|
let target_format = if frame_out {
|
||||||
runtime.surface.format
|
runtime.surface.format
|
||||||
} else {
|
} else {
|
||||||
let ExecutionKind::Render {
|
if !matches!(execution.kind, ExecutionKind::Fullscreen) {
|
||||||
color_attachments, ..
|
|
||||||
} = &execution.kind
|
|
||||||
else {
|
|
||||||
return Err(fail("fullscreen execution is not render"));
|
return Err(fail("fullscreen execution is not render"));
|
||||||
};
|
}
|
||||||
|
let (color_attachments, _) =
|
||||||
|
crate::render_graph::execution_attachments(execution);
|
||||||
let target = color_attachments
|
let target = color_attachments
|
||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| fail("fullscreen target missing"))?
|
.ok_or_else(|| fail("fullscreen target missing"))?
|
||||||
@@ -2045,21 +2041,17 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
executions.push(PreparedExecution::Fullscreen {
|
executions.push(PreparedExecution::Fullscreen {
|
||||||
execution: index,
|
|
||||||
frame_out,
|
|
||||||
bind_group,
|
bind_group,
|
||||||
pipeline,
|
pipeline,
|
||||||
_uniform: uniform,
|
_uniform: uniform,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"pipeline" => {
|
_ if contract.is_raster_draw() => {
|
||||||
let ExecutionKind::Render {
|
if !matches!(execution.kind, ExecutionKind::RasterDraw) {
|
||||||
color_attachments,
|
|
||||||
depth_stencil,
|
|
||||||
} = &execution.kind
|
|
||||||
else {
|
|
||||||
return Err(fail("pipeline is not render"));
|
return Err(fail("pipeline is not render"));
|
||||||
};
|
}
|
||||||
|
let (color_attachments, depth_stencil) =
|
||||||
|
crate::render_graph::execution_attachments(execution);
|
||||||
let color = color_attachments
|
let color = color_attachments
|
||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| fail("pipeline color missing"))?;
|
.ok_or_else(|| fail("pipeline color missing"))?;
|
||||||
@@ -2098,8 +2090,8 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
.ok_or_else(|| fail("depth allocation invalid"))
|
.ok_or_else(|| fail("depth allocation invalid"))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let NormalizedParameters::Pipeline {
|
let NormalizedParameters::Raster {
|
||||||
pipeline: _,
|
draw_order: _,
|
||||||
depth_compare,
|
depth_compare,
|
||||||
depth_write_enabled,
|
depth_write_enabled,
|
||||||
..
|
..
|
||||||
@@ -2145,7 +2137,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
)
|
)
|
||||||
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
||||||
executions.push(PreparedExecution::Pipeline {
|
executions.push(PreparedExecution::Pipeline {
|
||||||
execution: index,
|
|
||||||
base,
|
base,
|
||||||
predicate_ordinal: runtime
|
predicate_ordinal: runtime
|
||||||
.instance_traversal
|
.instance_traversal
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ use std::{
|
|||||||
};
|
};
|
||||||
use wasm_bindgen::JsValue;
|
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))]
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
const SLOT_COUNT: usize = 4;
|
const SLOT_COUNT: usize = 4;
|
||||||
#[cfg(any(target_arch = "wasm32", test))]
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
const QUERY_COUNT: u32 = (MAX_PROFILE_PASSES * 2) as u32;
|
const QUERY_COUNT: u32 = (MAX_PROFILE_PASSES * 2) as u32;
|
||||||
#[cfg(any(target_arch = "wasm32", test))]
|
#[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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum SlotState {
|
enum SlotState {
|
||||||
@@ -396,7 +399,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn capacity_is_aligned() {
|
fn capacity_is_aligned() {
|
||||||
assert_eq!(RESOLVE_SIZE % wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT, 0);
|
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]
|
#[test]
|
||||||
fn lazy_identity_when_disabled_unavailable_or_full() {
|
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([
|
const GROUPS = Object.freeze([
|
||||||
["source", "Source"],
|
["source", "Source"],
|
||||||
@@ -16,7 +16,7 @@ export const addNodeItems = Object.freeze(
|
|||||||
GROUPS.flatMap(([execution, group]) =>
|
GROUPS.flatMap(([execution, group]) =>
|
||||||
Object.entries(semanticCatalog)
|
Object.entries(semanticCatalog)
|
||||||
.filter(([, definition]) => definition.execution === execution)
|
.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 GRAPH_ID = "authored_gpu_culling";
|
||||||
export const CATALOG_VERSION = 11;
|
export const CATALOG_VERSION = 12;
|
||||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||||
const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" : "parameter_literal", maximum = 1) => ({
|
const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" : "parameter_literal", maximum = 1) => ({
|
||||||
accepted: typeof type === "string" ? exact(type) : type,
|
accepted: typeof type === "string" ? exact(type) : type,
|
||||||
@@ -63,6 +63,20 @@ const texture = {
|
|||||||
viewFormats: [],
|
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({
|
export const semanticCatalog = Object.freeze({
|
||||||
mesh: {
|
mesh: {
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -107,18 +121,9 @@ export const semanticCatalog = Object.freeze({
|
|||||||
outputs: { isFrustumCulled: o("bool") },
|
outputs: { isFrustumCulled: o("bool") },
|
||||||
parameters: { cameraSelection: "active" },
|
parameters: { cameraSelection: "active" },
|
||||||
},
|
},
|
||||||
pipeline: {
|
ground_plane: raster(),
|
||||||
version: 4,
|
gltf_standard: raster(),
|
||||||
execution: "render",
|
gltf_standard_double_sided: raster(),
|
||||||
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] },
|
|
||||||
},
|
|
||||||
...expressionCatalog,
|
...expressionCatalog,
|
||||||
fullscreen_copy: {
|
fullscreen_copy: {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -292,7 +297,6 @@ const enumeration = (value, values) => ({
|
|||||||
default: tagged("string", value),
|
default: tagged("string", value),
|
||||||
enum: values,
|
enum: values,
|
||||||
});
|
});
|
||||||
const string = (value) => ({ type: "string", default: tagged("string", value) });
|
|
||||||
const boolean = (value) => ({
|
const boolean = (value) => ({
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: tagged("boolean", value),
|
default: tagged("boolean", value),
|
||||||
@@ -327,7 +331,7 @@ const zero = (type) => {
|
|||||||
Array.from({ length: size }, (_, row) => Number(column === row)));
|
Array.from({ length: size }, (_, row) => Number(column === row)));
|
||||||
};
|
};
|
||||||
const defaultForInput = (key, name, type) => {
|
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 (key === "and") return true;
|
||||||
if (/^combine_mat[234]$/.test(key)) {
|
if (/^combine_mat[234]$/.test(key)) {
|
||||||
const index = Number(name.replace("column", ""));
|
const index = Number(name.replace("column", ""));
|
||||||
@@ -374,8 +378,8 @@ const parameterSchemas = {
|
|||||||
},
|
},
|
||||||
mesh: {},
|
mesh: {},
|
||||||
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
||||||
pipeline: {
|
ground_plane: {
|
||||||
pipeline: string("gltf_standard"),
|
drawOrder: { ...number(0, -2147483648, 2147483647), integer: true },
|
||||||
depthCompare: enumeration("less_equal", [
|
depthCompare: enumeration("less_equal", [
|
||||||
"never",
|
"never",
|
||||||
"less",
|
"less",
|
||||||
@@ -418,6 +422,8 @@ const parameterSchemas = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
for (const key of Object.keys(expressionCatalog)) parameterSchemas[key] = {};
|
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(
|
export const nodeDefinitions = Object.fromEntries(
|
||||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||||
const sockets = {
|
const sockets = {
|
||||||
@@ -453,7 +459,7 @@ export const nodeDefinitions = Object.fromEntries(
|
|||||||
key,
|
key,
|
||||||
{
|
{
|
||||||
version: c.version,
|
version: c.version,
|
||||||
title: key.replaceAll("_", " "),
|
title: NODE_TITLE_OVERRIDES[key] ?? key.replaceAll("_", " "),
|
||||||
behavior: "standard",
|
behavior: "standard",
|
||||||
style: c.execution,
|
style: c.execution,
|
||||||
parameters,
|
parameters,
|
||||||
@@ -475,6 +481,13 @@ export const nodeDefinitions = Object.fromEntries(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
nodeDefinitions.mesh.sockets.localAabb.title = "Local AABB";
|
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 = [
|
nodeDefinitions.color_balance.ui = [
|
||||||
{ kind: "parameter", parameter: "mode" },
|
{ kind: "parameter", parameter: "mode" },
|
||||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
{ 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"));
|
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" } };
|
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 classification = predicates(withCulling);
|
||||||
const explicit = colorTarget || heightScale !== 1;
|
|
||||||
const target = colorTarget || "hdr";
|
const target = colorTarget || "hdr";
|
||||||
return [
|
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"),
|
node("mesh", "mesh"),
|
||||||
...classification.nodes,
|
...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("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", "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", "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", "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("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, [
|
const direct = (graphId, clearColor) => graph(graphId, [
|
||||||
node("ldr", "texture", texture("rgba8_unorm")),
|
node("ldr", "texture", texture("rgba8_unorm")),
|
||||||
...scene("ldr", clearColor),
|
...scene("ldr", clearColor),
|
||||||
@@ -58,7 +58,7 @@ export const hdr = graph("preset_hdr_fullscreen", [
|
|||||||
]);
|
]);
|
||||||
export const msaa = graph("preset_msaa", [
|
export const msaa = graph("preset_msaa", [
|
||||||
node("msaa_hdr", "texture", texture("rgba16_float", 1, 1, 4)),
|
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") }),
|
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
||||||
]);
|
]);
|
||||||
export const culling = graph("preset_gpu_culling", (() => {
|
export const culling = graph("preset_gpu_culling", (() => {
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
} from "../static/render-graph/node-spawn.js";
|
} from "../static/render-graph/node-spawn.js";
|
||||||
|
|
||||||
test("add-node model contains all final catalog types in application groups", () => {
|
test("add-node model contains all final catalog types in application groups", () => {
|
||||||
assert.equal(addNodeItems.length, 42);
|
assert.equal(addNodeItems.length, 44);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
[...new Set(addNodeItems.map((item) => item.group))],
|
[...new Set(addNodeItems.map((item) => item.group))],
|
||||||
["Source", "Expression", "Render / post", "Frame"],
|
["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) => item.typeId === "separate_u32_bits" && item.group === "Expression"));
|
||||||
assert.ok(!addNodeItems.some((item) => ["mesh_query", "pipeline_registry"].includes(item.typeId)));
|
assert.ok(!addNodeItems.some((item) => ["mesh_query", "pipeline_registry"].includes(item.typeId)));
|
||||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
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),
|
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||||
);
|
);
|
||||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||||
assert.equal(fxNodeComposition.version, 11);
|
assert.equal(fxNodeComposition.version, 12);
|
||||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 42);
|
assert.equal(Object.keys(fxNodeComposition.nodes).length, 44);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Object.values(fxNodeComposition.nodes).every(
|
Object.values(fxNodeComposition.nodes).every(
|
||||||
(definition) => definition.migrations.length === 0,
|
(definition) => definition.migrations.length === 0,
|
||||||
|
|||||||
@@ -30,15 +30,18 @@ const authoredLink = (id, from, to = "target", muted = false) => ({
|
|||||||
toNodeId: to, toSocketId: `${to}:inputs`, muted, extensions: {},
|
toNodeId: to, toSocketId: `${to}:inputs`, muted, extensions: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("catalog v11 exposes the final mesh, pipeline, and typed-expression contracts", () => {
|
test("catalog v12 exposes raster and typed-expression contracts", () => {
|
||||||
assert.equal(CATALOG_VERSION, 11);
|
assert.equal(CATALOG_VERSION, 12);
|
||||||
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
||||||
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
||||||
});
|
});
|
||||||
assert.equal(semanticCatalog.mesh.version, 2);
|
assert.equal(semanticCatalog.mesh.version, 2);
|
||||||
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
||||||
assert.equal(semanticCatalog.pipeline.version, 4);
|
assert.equal(semanticCatalog.pipeline, undefined);
|
||||||
assert.deepEqual(semanticCatalog.pipeline.inputs.predicate.cardinality, { minimum: 0, maximum: 1 });
|
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.deepEqual(semanticCatalog.and.inputs.inputs.cardinality, { minimum: 0, maximum: 8 });
|
||||||
assert.equal(nodeDefinitions.and.sockets.inputs.maxIncomingLinks, 8);
|
assert.equal(nodeDefinitions.and.sockets.inputs.maxIncomingLinks, 8);
|
||||||
assert.equal(nodeDefinitions.and.sockets.inputs.value, null);
|
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", () => {
|
test("current culling fixture uses type-bit predicates and final socket versions", () => {
|
||||||
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
||||||
assert.deepEqual(byId.cull.inputs.localAabb, [{ node: "mesh", socket: "localAabb" }]);
|
assert.deepEqual(byId.cull.inputs.localAabb, [{ node: "mesh", socket: "localAabb" }]);
|
||||||
assert.deepEqual(byId.type_words.inputs.value, [{ node: "mesh", socket: "type" }]);
|
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");
|
assert.equal(byId.ground.inputs.predicate[0].node, "ground_class");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("compiler texture sockets expose policy metadata without literal widgets", () => {
|
test("compiler texture sockets expose policy metadata without literal widgets", () => {
|
||||||
for (const socket of ["colorTarget", "depthTarget"]) {
|
for (const socket of ["colorTarget", "depthTarget"]) {
|
||||||
assert.equal(semanticCatalog.pipeline.inputs[socket].defaultPolicy, "compiler_texture");
|
assert.equal(semanticCatalog.gltf_standard.inputs[socket].defaultPolicy, "compiler_texture");
|
||||||
assert.equal(nodeDefinitions.pipeline.sockets[socket].default, undefined);
|
assert.equal(nodeDefinitions.gltf_standard.sockets[socket].default, undefined);
|
||||||
}
|
}
|
||||||
assert.equal(semanticCatalog.pipeline.inputs.predicate.defaultPolicy, "parameter_literal");
|
assert.equal(semanticCatalog.gltf_standard.inputs.predicate.defaultPolicy, "parameter_literal");
|
||||||
assert.notEqual(nodeDefinitions.pipeline.sockets.predicate.default, null);
|
assert.notEqual(nodeDefinitions.gltf_standard.sockets.predicate.default, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("removed architecture is absent from the authoring catalog", () => {
|
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", () => {
|
test("all presets use current schemas, versions, and one frame output", () => {
|
||||||
assert.equal(Object.keys(presets.renderGraphPresets).length, 13);
|
assert.equal(Object.keys(presets.renderGraphPresets).length, 13);
|
||||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
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(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);
|
assert.equal(graph.nodes.filter((node) => node.executor.key === "frame_out").length, 1, name);
|
||||||
for (const node of graph.nodes)
|
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]) =>
|
const authoredX4 = Object.entries(presets.renderGraphPresets).flatMap(([name, graph]) =>
|
||||||
graph.nodes.filter((node) => node.parameters?.texture?.sampleCount === 4)
|
graph.nodes.filter((node) => node.parameters?.texture?.sampleCount === 4)
|
||||||
.map((node) => `${name}:${node.id}`));
|
.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")
|
assert.equal(typeof presets.msaa.nodes.find((node) => node.id === "msaa_hdr")
|
||||||
.parameters.texture.sampleCount, "number");
|
.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);
|
assert.deepEqual(byId.pbr_double.inputs.predicate, [{ node: "double_class", socket: "value" }], name);
|
||||||
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
||||||
assert.deepEqual(pipeline.inputs.mesh, [{ node: "mesh", socket: "mesh" }]);
|
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");
|
assert.equal(byId[id].inputs.inputs.at(-1).node, "not_culled");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("implicit presets start disconnected and then chain both attachments", () => {
|
test("scene pipelines directly share matching explicit color and depth targets", () => {
|
||||||
for (const name of ["hdr", "culling", "grading"]) {
|
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||||
const byId = Object.fromEntries(presets[name].nodes.map((node) => [node.id, node]));
|
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
||||||
assert.equal("colorTarget" in byId.ground.inputs, false, name);
|
const color = byId.ground.inputs.colorTarget;
|
||||||
assert.equal("depthTarget" in byId.ground.inputs, false, name);
|
const depth = byId.ground.inputs.depthTarget;
|
||||||
assert.deepEqual(byId.pbr.inputs.colorTarget, [{ node: "ground", socket: "color" }], name);
|
for (const id of ["ground", "pbr", "pbr_double"]) {
|
||||||
assert.deepEqual(byId.pbr.inputs.depthTarget, [{ node: "ground", socket: "depth" }], name);
|
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