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:
Amp
2026-07-29 14:49:22 +00:00
co-authored by heaust
parent 27e3dd64ae
commit 780f194be4
16 changed files with 1519 additions and 376 deletions
+564 -59
View File
@@ -21,8 +21,8 @@ struct CullParameters {
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct PipelineParameters {
pipeline: String,
struct RasterParameters {
draw_order: i32,
depth_compare: CompareFunction,
depth_write_enabled: bool,
clear_depth: f32,
@@ -137,6 +137,11 @@ fn color(value: [f32; 4], min: f32, max: f32, base: &str) -> Result<[f32; 3], Gr
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
struct OutputKey(usize, u16);
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
enum AttachmentRoot {
Authored(OutputKey),
CompilerDefault { node: usize, ordinal: u16 },
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
enum TransitionTargetKey {
Authored(OutputKey),
CompilerDefaultInput {
@@ -244,6 +249,7 @@ fn reaches(
from: usize,
to: usize,
outgoing_edges: &[Vec<usize>],
ordering_outgoing: &[Vec<usize>],
edges: &[DependencyEdge],
live: &HashSet<usize>,
memo: &mut HashMap<(usize, usize), bool>,
@@ -268,6 +274,11 @@ fn reaches(
stack.push(next);
}
}
for &next in &ordering_outgoing[node] {
if live.contains(&next) {
stack.push(next);
}
}
}
memo.insert((from, to), answer);
answer
@@ -741,21 +752,9 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
descriptor: normalize_texture(p.texture, &base)?,
}
}
"pipeline" => {
let p: PipelineParameters =
key if contract(key).is_some_and(|contract| contract.is_raster_draw()) => {
let p: RasterParameters =
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
let valid_name = !p.pipeline.is_empty()
&& p.pipeline.len() <= 64
&& p.pipeline.bytes().enumerate().all(|(i, c)| {
c == b'_' || c.is_ascii_alphanumeric() && (i > 0 || c.is_ascii_alphabetic())
});
if !valid_name {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
"pipeline must be a 1-64 byte identifier",
format!("{base}.pipeline"),
));
}
if !p.clear_depth.is_finite() || !(0.0..=1.0).contains(&p.clear_depth) {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
@@ -770,8 +769,8 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
format!("{base}.clearColor"),
));
}
NormalizedParameters::Pipeline {
pipeline: p.pipeline,
NormalizedParameters::Raster {
draw_order: p.draw_order,
depth_compare: p.depth_compare,
depth_write_enabled: p.depth_write_enabled,
clear_depth: p.clear_depth,
@@ -1102,6 +1101,271 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
e.producer_output_ordinal,
)
});
let is_normalizable_target = |edge: &DependencyEdge| {
contracts[edge.from_node].is_raster_draw()
&& contracts[edge.to_node].is_raster_draw()
&& matches!(
contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize].role,
InputRole::ColorTarget { .. } | InputRole::DepthTarget
)
};
let is_exact_reader = |edge: &DependencyEdge| {
let input = &contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize];
input.role == InputRole::SampledTexture
|| (input.role == InputRole::SemanticRead
&& contracts[edge.from_node].outputs[edge.producer_output_ordinal as usize]
.semantic_type
== SemanticType::Texture)
};
// Compute demand from authored resource dependencies only. In particular,
// a later WAR ordering edge must never resurrect its reader.
let mut provisional_live = HashSet::new();
let mut provisional_stack: Vec<_> = contracts
.iter()
.enumerate()
.filter(|(i, contract)| {
contract.inherently_observable && graph.nodes[*i].state == NodeState::Enabled
})
.map(|(i, _)| i)
.collect();
let mut authored_deps = vec![Vec::new(); graph.nodes.len()];
for edge in &edges {
authored_deps[edge.to_node].push(edge.from_node);
}
while let Some(node) = provisional_stack.pop() {
if provisional_live.insert(node) {
provisional_stack.extend(authored_deps[node].iter().copied());
}
}
// Preserve authored, non-normalized dataflow before attachment normalization. It
// is used both to diagnose impossible draw-order normalization and to find
// readers which must finish before an attachment version is overwritten.
let ordinary_edges: Vec<_> = edges
.iter()
.filter(|edge| !is_normalizable_target(edge))
.cloned()
.collect();
let mut target_consumers = HashMap::<OutputKey, Vec<usize>>::new();
let mut ordinary_consumers = HashMap::<OutputKey, Vec<usize>>::new();
for edge in &edges {
let key = OutputKey(edge.from_node, edge.producer_output_ordinal);
if is_normalizable_target(edge) {
target_consumers.entry(key).or_default().push(edge.to_node);
} else if is_exact_reader(edge) && provisional_live.contains(&edge.to_node) {
ordinary_consumers
.entry(key)
.or_default()
.push(edge.to_node);
}
}
let observation_cuts: HashSet<_> = target_consumers
.keys()
.filter(|key| ordinary_consumers.contains_key(key))
.copied()
.collect();
let mut ordering_edges = Vec::new();
// Normalize raster attachment versions before liveness. Authored graphs may
// express a render-pass cohort either as a chain or as direct siblings of
// one texture. Turn both forms into an explicit, deterministic SSA chain.
fn attachment_root(
node: usize,
ordinal: u16,
bound: &[BTreeMap<&str, BoundInput>],
contracts: &[&Contract],
colors: &mut HashMap<(usize, u16), u8>,
roots: &mut HashMap<(usize, u16), AttachmentRoot>,
) -> Result<AttachmentRoot, GraphError> {
if let Some(&root) = roots.get(&(node, ordinal)) {
return Ok(root);
}
if colors.get(&(node, ordinal)) == Some(&1) {
return Err(error(
"GRAPH_ATTACHMENT_LINEAGE_INVALID",
"attachment lineage is cyclic",
format!("nodes[{node}].inputs"),
));
}
colors.insert((node, ordinal), 1);
let socket = if ordinal == 0 {
"colorTarget"
} else {
"depthTarget"
};
let root = match bound[node].get(socket).map(|binding| binding.producer) {
None => AttachmentRoot::CompilerDefault { node, ordinal },
Some(output)
if !contracts[output.0].is_raster_draw()
&& contracts[output.0].outputs[output.1 as usize].semantic_type
== SemanticType::Texture =>
{
AttachmentRoot::Authored(output)
}
Some(output) if contracts[output.0].is_raster_draw() && output.1 == ordinal => {
attachment_root(output.0, ordinal, bound, contracts, colors, roots)?
}
Some(_) => {
return Err(error(
"GRAPH_ATTACHMENT_LINEAGE_INVALID",
"attachment lineage has no texture or default root",
format!("nodes[{node}].inputs.{socket}"),
))
}
};
colors.insert((node, ordinal), 2);
roots.insert((node, ordinal), root);
Ok(root)
}
let raster_nodes: Vec<_> = contracts
.iter()
.enumerate()
.filter_map(|(i, contract)| contract.is_raster_draw().then_some(i))
.collect();
let mut aliases = HashMap::<OutputKey, OutputKey>::new();
for ordinal in 0..=1u16 {
let mut colors = HashMap::new();
let mut roots = HashMap::new();
let mut cohorts = BTreeMap::<AttachmentRoot, Vec<usize>>::new();
for &node in &raster_nodes {
let root = attachment_root(node, ordinal, &bound, &contracts, &mut colors, &mut roots)?;
cohorts.entry(root).or_default().push(node);
}
for (root, mut members) in cohorts {
members.sort_by_key(|&node| {
let NormalizedParameters::Raster { draw_order, .. } = params[node] else {
unreachable!()
};
(draw_order, node)
});
// An observed version ends its aliasing segment. Writers after the
// cut continue the same physical lineage, but may not replace the
// version seen by the reader.
let mut segment_start = 0;
for position in 0..members.len() {
let at_cut = observation_cuts.contains(&OutputKey(members[position], ordinal));
if at_cut || position + 1 == members.len() {
let terminal = OutputKey(members[position], ordinal);
for &member in &members[segment_start..=position] {
aliases.insert(OutputKey(member, ordinal), terminal);
}
if let Some(&next_writer) = members.get(position + 1) {
for &member in &members[segment_start..=position] {
let output = OutputKey(member, ordinal);
for &reader in ordinary_consumers.get(&output).into_iter().flatten() {
if reader != next_writer {
ordering_edges.push((reader, next_writer));
}
}
}
}
segment_start = position + 1;
}
}
let socket = if ordinal == 0 {
"colorTarget"
} else {
"depthTarget"
};
for (position, &member) in members.iter().enumerate() {
let target = if position == 0 {
match root {
AttachmentRoot::Authored(output) => Some(output),
AttachmentRoot::CompilerDefault { .. } => None,
}
} else {
Some(OutputKey(members[position - 1], ordinal))
};
bound[member].remove(socket);
if let Some(producer) = target {
bound[member].insert(socket, BoundInput { producer });
}
}
}
}
ordering_edges.sort_unstable();
ordering_edges.dedup();
// Replace authored attachment dependencies with the canonical WAW chain,
// and redirect observations of any cohort member to its terminal version.
edges.retain(|edge| {
!(contracts[edge.to_node].is_raster_draw()
&& (edge.to_socket == "colorTarget" || edge.to_socket == "depthTarget"))
});
for &node in &raster_nodes {
for (socket, ordinal) in [("colorTarget", 0u16), ("depthTarget", 1u16)] {
let Some(binding) = bound[node].get(socket) else {
continue;
};
let input_ordinal = contracts[node]
.inputs
.iter()
.position(|input| input.name == socket)
.expect("raster target contract") as u16;
edges.push(DependencyEdge {
from_node: binding.producer.0,
from_socket: contracts[binding.producer.0].outputs[binding.producer.1 as usize]
.name
.into(),
producer_output_ordinal: binding.producer.1,
to_node: node,
to_socket: socket.into(),
consumer_input_ordinal: input_ordinal,
resource: NodeOutputRef {
node: graph.nodes[binding.producer.0].id.clone(),
socket: contracts[binding.producer.0].outputs[binding.producer.1 as usize]
.name
.into(),
},
});
let _ = ordinal;
}
}
for node in 0..graph.nodes.len() {
for input in contracts[node].inputs.iter().filter(|input| {
matches!(
input.role,
InputRole::SampledTexture | InputRole::SemanticRead
)
}) {
let Some(binding) = bound[node].get_mut(input.name) else {
continue;
};
if input.role == InputRole::SemanticRead
&& contracts[binding.producer.0].outputs[binding.producer.1 as usize].semantic_type
!= SemanticType::Texture
{
continue;
}
if let Some(&terminal) = aliases.get(&binding.producer) {
binding.producer = terminal;
}
}
}
for edge in &mut edges {
if is_exact_reader(edge) {
let key = OutputKey(edge.from_node, edge.producer_output_ordinal);
if let Some(&terminal) = aliases.get(&key) {
edge.from_node = terminal.0;
edge.producer_output_ordinal = terminal.1;
edge.from_socket = contracts[terminal.0].outputs[terminal.1 as usize]
.name
.into();
edge.resource = NodeOutputRef {
node: graph.nodes[terminal.0].id.clone(),
socket: edge.from_socket.clone(),
};
}
}
}
edges.sort_by_key(|e| {
(
e.to_node,
e.consumer_input_ordinal,
e.from_node,
e.producer_output_ordinal,
)
});
let mut deps = vec![Vec::new(); graph.nodes.len()];
for edge in &edges {
deps[edge.to_node].push(edge.from_node);
@@ -1122,6 +1386,42 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
stack.extend(deps[i].iter().copied());
}
}
// A canonical attachment writer edge may not reverse authored ordinary
// dataflow. Keep this distinct from generic cycle reporting so authors get
// the draw-order parameter which made normalization impossible.
let mut ordinary_outgoing = vec![Vec::new(); graph.nodes.len()];
for edge in &ordinary_edges {
ordinary_outgoing[edge.from_node].push(edge.to_node);
}
let ordinary_reaches = |from: usize, to: usize| {
let mut seen = vec![false; graph.nodes.len()];
let mut pending = vec![from];
while let Some(node) = pending.pop() {
if node == to {
return true;
}
if !seen[node] {
seen[node] = true;
pending.extend(ordinary_outgoing[node].iter().copied());
}
}
false
};
let mut draw_order_conflicts: Vec<_> = edges
.iter()
.filter(|edge| {
contracts[edge.from_node].is_raster_draw()
&& contracts[edge.to_node].is_raster_draw()
&& matches!(
contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize].role,
InputRole::ColorTarget { .. } | InputRole::DepthTarget
)
&& ordinary_reaches(edge.to_node, edge.from_node)
})
.map(|edge| edge.to_node)
.collect();
draw_order_conflicts.sort_unstable();
draw_order_conflicts.dedup();
// IDs are independent of scheduling: original node order, then contract output order.
// Source nodes expose only outputs that survived active-edge/liveness analysis;
// executable nodes retain their complete output shape for runtime lowering.
@@ -1215,10 +1515,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let mut default_roots: Vec<DefaultDraft> = Vec::new();
let mut default_targets = HashMap::new();
for i in 0..graph.nodes.len() {
if !live.contains(&i) || contracts[i].key != "pipeline" {
if !live.contains(&i) || !contracts[i].is_raster_draw() {
continue;
}
for (input_ordinal, (socket, role, format, opposite)) in [
for (socket, role, format, opposite) in [
(
"colorTarget",
CompilerTextureRole::ColorTarget,
@@ -1233,12 +1533,16 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
),
]
.into_iter()
.enumerate()
{
if bound[i].contains_key(socket) {
continue;
}
let input_ordinal = input_ordinal as u16 + 2;
let input_ordinal = contracts[i]
.inputs
.iter()
.position(|input| input.name == socket)
.expect("compiler target has a contract input")
as u16;
let key = TransitionTargetKey::CompilerDefaultInput {
owner_node: i,
input_ordinal,
@@ -1267,8 +1571,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
if !live.contains(&i) {
continue;
}
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
"pipeline" => &[("colorTarget", 0), ("depthTarget", 1)],
let transition_sockets: &[(&str, u16)] = match contracts[i] {
ref contract if contract.is_raster_draw() => &[("colorTarget", 0), ("depthTarget", 1)],
_ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)],
_ => continue,
};
@@ -1388,6 +1692,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
outgoing_edges[edge.from_node].push(index);
}
}
let mut ordering_outgoing = vec![Vec::new(); graph.nodes.len()];
for &(from, to) in &ordering_edges {
if live.contains(&from) && live.contains(&to) {
ordering_outgoing[from].push(to);
}
}
for outgoing in &mut outgoing_edges {
outgoing.sort_by_key(|&index| {
let edge = &edges[index];
@@ -1404,7 +1714,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
if !live.contains(&i) {
continue;
}
if contracts[i].key == "pipeline" {
if contracts[i].is_raster_draw() {
if bound[i].get("colorTarget").map(|b| b.producer)
== bound[i].get("depthTarget").map(|b| b.producer)
&& bound[i].contains_key("colorTarget")
@@ -1532,7 +1842,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket),
));
}
if contracts[edge.from_node].key != "pipeline" || edge.producer_output_ordinal != 0 {
if !contracts[edge.from_node].is_raster_draw() || edge.producer_output_ordinal != 0 {
return Err(error(
"GRAPH_ILLEGAL_ACCESS",
"multisampled color texture is not a produced pipeline color",
@@ -1571,6 +1881,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
i,
next.writer_node,
&outgoing_edges,
&ordering_outgoing,
&edges,
&live,
&mut reachability,
@@ -1587,7 +1898,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
// Validate every independently resolved attachment before graph cycle reporting.
for i in 0..graph.nodes.len() {
if !live.contains(&i) || contracts[i].key != "pipeline" {
if !live.contains(&i) || !contracts[i].is_raster_draw() {
continue;
}
let cd = version_of
@@ -1780,12 +2091,23 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
// Stable Kahn scheduling is deliberately after resource and access validation.
if let Some(&node) = draw_order_conflicts.iter().find(|node| live.contains(node)) {
return Err(error(
"GRAPH_DRAW_ORDER_CONFLICT",
"draw order conflicts with authored dependencies",
format!("nodes[{node}].parameters.drawOrder"),
));
}
let mut indegree = vec![0; graph.nodes.len()];
for &node in &live {
indegree[node] = edges
.iter()
.filter(|edge| edge.to_node == node && live.contains(&edge.from_node))
.count();
.count()
+ ordering_edges
.iter()
.filter(|(from, to)| *to == node && live.contains(from))
.count();
}
let mut queue = BinaryHeap::new();
for &node in &live {
@@ -1803,6 +2125,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
queue.push(Reverse(consumer));
}
}
for &consumer in &ordering_outgoing[node] {
indegree[consumer] -= 1;
if indegree[consumer] == 0 {
queue.push(Reverse(consumer));
}
}
}
if order.len() != live.len() {
let residual: Vec<_> = (0..graph.nodes.len())
@@ -1811,6 +2139,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
fn cycle_dfs(
node: usize,
outgoing_edges: &[Vec<usize>],
ordering_outgoing: &[Vec<usize>],
edges: &[DependencyEdge],
residual: &[bool],
colors: &mut [u8],
@@ -1829,6 +2158,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
if let Some(cycle) = cycle_dfs(
to,
outgoing_edges,
ordering_outgoing,
edges,
residual,
colors,
@@ -1848,6 +2178,31 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
return Some(cycle);
}
}
for &to in &ordering_outgoing[node] {
if !residual[to] {
continue;
}
if colors[to] == 0 {
if cycle_dfs(
to,
outgoing_edges,
ordering_outgoing,
edges,
residual,
colors,
node_stack,
edge_stack,
)
.is_some()
{
// Ordering edges are synthetic and have no authored
// socket payload, but still constitute a real cycle.
return Some(Vec::new());
}
} else if colors[to] == 1 {
return Some(Vec::new());
}
}
node_stack.pop();
colors[node] = 2;
None
@@ -1859,6 +2214,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
cycle = cycle_dfs(
node,
&outgoing_edges,
&ordering_outgoing,
&edges,
&residual,
&mut colors,
@@ -2189,15 +2545,15 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
.collect();
let mut accesses = Vec::new();
let kind = match contracts[i].key {
"pipeline" => {
_ if contracts[i].is_raster_draw() => {
let color = output_ids[&OutputKey(i, 0)];
let depth = output_ids[&OutputKey(i, 1)];
let clear = match params[i] {
NormalizedParameters::Pipeline { clear_color, .. } => clear_color,
NormalizedParameters::Raster { clear_color, .. } => clear_color,
_ => unreachable!(),
};
let clear_depth = match &params[i] {
NormalizedParameters::Pipeline { clear_depth, .. } => *clear_depth,
NormalizedParameters::Raster { clear_depth, .. } => *clear_depth,
_ => unreachable!(),
};
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
@@ -2280,20 +2636,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
},
});
}
ExecutionKind::Render {
color_attachments: vec![ColorAttachmentPlan {
resource: color,
resolve_target,
location: 0,
load: cl,
store: source_store,
}],
depth_stencil: Some(DepthStencilAttachmentPlan {
resource: depth,
load: dl,
store: StoreOp::Store,
}),
}
ExecutionKind::RasterDraw
}
_ if contracts[i].fullscreen_policy.is_some() => {
let color = output_ids[&OutputKey(i, 0)];
@@ -2321,16 +2664,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
full_overwrite: true,
},
});
ExecutionKind::Render {
color_attachments: vec![ColorAttachmentPlan {
resource: color,
resolve_target: None,
location: 0,
load,
store: StoreOp::Store,
}],
depth_stencil: None,
}
ExecutionKind::Fullscreen
}
"frame_out" => {
let r = input_resource("color");
@@ -2584,7 +2918,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let mut predicates = Vec::new();
for (execution, compiled) in executions.iter().enumerate() {
let node = compiled.original_node_index as usize;
if contracts[node].key != "pipeline" {
if !contracts[node].is_raster_draw() {
continue;
}
let mesh = output_ids[&bound[node]["mesh"].producer];
@@ -2599,7 +2933,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let predicate = if let Some(binding) = bound[node].get("predicate") {
expression_ids[&binding.producer]
} else {
let NormalizedParameters::Pipeline {
let NormalizedParameters::Raster {
predicate_default, ..
} = params[node]
else {
@@ -2651,9 +2985,15 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
}
}
// Dense lifetimes touch bindings, outputs, and accesses.
let render_passes = build_render_passes(&executions, &resources, &families);
let execution_pass: Vec<u32> = render_passes
.iter()
.enumerate()
.flat_map(|(pass, value)| value.executions.iter().map(move |_| pass as u32))
.collect();
// Dense lifetimes use physical pass ordinals; producer metadata remains logical.
for (ordinal, e) in executions.iter().enumerate() {
let ordinal = ordinal as u32;
let ordinal = execution_pass[ordinal];
let mut touched = BTreeSet::new();
for x in &e.inputs {
touched.insert(x.resource);
@@ -2710,6 +3050,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
node_count: graph.nodes.len() as u32,
resources,
executions,
render_passes,
texture_families: families,
allocation_classes: classes,
culled_node_count: (graph.nodes.len() - live.len()) as u32,
@@ -2719,6 +3060,170 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
})
}
pub(crate) fn execution_attachments(
execution: &CompiledExecution,
) -> (Vec<ColorAttachmentPlan>, Option<DepthStencilAttachmentPlan>) {
let mut colors = Vec::new();
let mut depth = None;
for access in &execution.accesses {
match access.mode {
AccessMode::ColorAttachment {
location,
load,
store,
..
} => colors.push(ColorAttachmentPlan {
resource: access.resource,
resolve_target: execution.accesses.iter().find_map(|candidate| {
match candidate.mode {
AccessMode::ColorResolve {
source,
location: l,
} if source == access.resource && l == location => Some(candidate.resource),
_ => None,
}
}),
location,
load,
store,
}),
AccessMode::DepthAttachment { load, store, .. } => {
depth = Some(DepthStencilAttachmentPlan {
resource: access.resource,
load,
store,
})
}
_ => {}
}
}
colors.sort_by_key(|color| color.location);
(colors, depth)
}
pub(crate) fn build_render_passes(
executions: &[CompiledExecution],
resources: &[CompiledResource],
families: &[TextureFamily],
) -> Vec<PhysicalRenderPass> {
let family = |resource: u32| {
resources
.get(resource as usize)
.and_then(|resource| match resource.plan {
ResourcePlan::Texture { family, .. }
| ResourcePlan::TextureSource { family, .. } => Some(family),
_ => None,
})
};
let mut passes: Vec<PhysicalRenderPass> = Vec::new();
for (index, execution) in executions.iter().enumerate() {
if matches!(execution.kind, ExecutionKind::FrameOut { .. }) {
passes.push(PhysicalRenderPass {
executions: vec![index as u32],
kind: PhysicalRenderPassKind::Surface,
});
continue;
}
let (colors, depth) = execution_attachments(execution);
let mut merged = false;
if matches!(execution.kind, ExecutionKind::RasterDraw)
&& colors.iter().all(|v| v.load == NormalizedColorLoad::Load)
&& depth
.as_ref()
.is_none_or(|v| v.load == NormalizedDepthLoad::Load)
{
if let Some(PhysicalRenderPass {
executions: members,
kind:
PhysicalRenderPassKind::Texture {
color_attachments: previous_colors,
depth_stencil: previous_depth,
},
}) = passes.last_mut()
{
let previous = &executions[*members.last().unwrap() as usize];
let target_input = |socket: &str| {
execution
.inputs
.iter()
.find(|input| input.socket == socket)
.map(|input| input.resource)
};
let exact = previous.outputs.iter().any(|out| {
out.socket == "color" && Some(out.resource) == target_input("colorTarget")
}) && depth.as_ref().is_none_or(|_| {
previous.outputs.iter().any(|out| {
out.socket == "depth" && Some(out.resource) == target_input("depthTarget")
})
});
let compatible = previous_colors.len() == colors.len()
&& previous_colors.iter().zip(&colors).all(|(a, b)| {
a.location == b.location
&& family(a.resource) == family(b.resource)
&& family(a.resource).is_some_and(|f| {
family(b.resource).is_some_and(|next_family| {
families.get(f as usize).is_some_and(|first| {
families.get(next_family as usize).is_some_and(|next| {
family_descriptor(first) == family_descriptor(next)
})
})
})
})
})
&& match (previous_depth.as_ref(), depth.as_ref()) {
(None, None) => true,
(Some(a), Some(b)) => {
family(a.resource) == family(b.resource)
&& family(a.resource).is_some_and(|f| {
family(b.resource).is_some_and(|next_family| {
families.get(f as usize).is_some_and(|first| {
families.get(next_family as usize).is_some_and(|next| {
family_descriptor(first) == family_descriptor(next)
})
})
})
})
}
_ => false,
};
let no_resolve = previous_colors.iter().all(|v| v.resolve_target.is_none());
let no_external = previous.outputs.iter().all(|out| {
executions
.iter()
.enumerate()
.filter(|(_, e)| e.inputs.iter().any(|v| v.resource == out.resource))
.all(|(consumer, _)| consumer == index)
});
if exact && compatible && no_resolve && no_external {
for (physical, final_value) in previous_colors.iter_mut().zip(&colors) {
physical.resource = final_value.resource;
physical.resolve_target = final_value.resolve_target;
physical.store = final_value.store;
}
if let (Some(physical), Some(final_value)) =
(previous_depth.as_mut(), depth.as_ref())
{
physical.resource = final_value.resource;
physical.store = final_value.store;
}
members.push(index as u32);
merged = true;
}
}
}
if !merged {
passes.push(PhysicalRenderPass {
executions: vec![index as u32],
kind: PhysicalRenderPassKind::Texture {
color_attachments: colors,
depth_stencil: depth,
},
});
}
}
passes
}
fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
match e {
NormalizedTextureExtent::Absolute {
+18 -3
View File
@@ -95,6 +95,11 @@ pub struct Contract {
#[serde(skip)]
pub fullscreen_policy: Option<FullscreenPolicy>,
}
impl Contract {
pub const fn is_raster_draw(&self) -> bool {
matches!(self.execution, ExecutionClass::Render) && self.fullscreen_policy.is_none()
}
}
use SemanticType::*;
const R: InputCardinality = InputCardinality { min: 1, max: 1 };
@@ -138,7 +143,7 @@ const MESH_O: &[OutputSocketContract] = &[
o("localAabb", LocalAabb),
];
const TEXTURE_O: &[OutputSocketContract] = &[o("texture", Texture)];
const PIPE_I: &[InputSocketContract] = &[
const RASTER_I: &[InputSocketContract] = &[
i("mesh", MeshData, R, InputRole::SemanticRead),
i("predicate", Bool, O, InputRole::Expression),
i(
@@ -149,7 +154,7 @@ const PIPE_I: &[InputSocketContract] = &[
),
i("depthTarget", Texture, O, InputRole::DepthTarget),
];
const PIPE_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
const RASTER_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
const CULL_I: &[InputSocketContract] = &[
i("mesh", MeshData, R, InputRole::Expression),
i("localAabb", LocalAabb, R, InputRole::Expression),
@@ -202,7 +207,17 @@ pub static CONTRACTS: &[Contract] = &[
c!("mesh", 2, Source, NONE_I, MESH_O, false, None),
c!("texture", 2, Source, NONE_I, TEXTURE_O, false, None),
c!("frustum_cull", 2, Expression, CULL_I, CULL_O, false, None),
c!("pipeline", 4, Render, PIPE_I, PIPE_O, false, None),
c!("ground_plane", 1, Render, RASTER_I, RASTER_O, false, None),
c!("gltf_standard", 1, Render, RASTER_I, RASTER_O, false, None),
c!(
"gltf_standard_double_sided",
1,
Render,
RASTER_I,
RASTER_O,
false,
None
),
c!(
"and",
2,
+1
View File
@@ -1,6 +1,7 @@
//! Device-free render graph compiler and compiled graph registry.
mod compiler;
pub(crate) use compiler::execution_attachments;
mod contracts;
mod expression;
mod plan;
+23 -9
View File
@@ -11,6 +11,7 @@ pub struct CompiledGraph {
pub node_count: u32,
pub resources: Vec<CompiledResource>,
pub executions: Vec<CompiledExecution>,
pub render_passes: Vec<PhysicalRenderPass>,
pub texture_families: Vec<TextureFamily>,
pub allocation_classes: Vec<AllocationClass>,
pub culled_node_count: u32,
@@ -108,16 +109,29 @@ pub struct CompiledSocketOutput {
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ExecutionKind {
Render {
RasterDraw,
Fullscreen,
FrameOut { color: u32 },
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PhysicalRenderPass {
pub executions: Vec<u32>,
pub kind: PhysicalRenderPassKind,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PhysicalRenderPassKind {
Texture {
color_attachments: Vec<ColorAttachmentPlan>,
depth_stencil: Option<DepthStencilAttachmentPlan>,
},
FrameOut {
color: u32,
},
Surface,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorAttachmentPlan {
pub resource: u32,
@@ -127,7 +141,7 @@ pub struct ColorAttachmentPlan {
pub store: StoreOp,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DepthStencilAttachmentPlan {
pub resource: u32,
@@ -206,8 +220,8 @@ pub enum NormalizedParameters {
ExpressionDefaults {
defaults: Vec<TypedLiteral>,
},
Pipeline {
pipeline: String,
Raster {
draw_order: i32,
depth_compare: CompareFunction,
depth_write_enabled: bool,
clear_depth: f32,
@@ -472,6 +486,6 @@ pub enum TextureUsage {
impl CompiledGraph {
pub fn summary(&self, id: [u32; 2]) -> serde_json::Value {
serde_json::json!({"compiledId":id,"graphId":self.graph_id,"revision":self.revision,"schemaVersion":self.schema_version,"nodeCount":self.node_count,"executionCount":self.executions.len(),"resourceCount":self.resources.len(),"culledNodeCount":self.culled_node_count,"culledResourceCount":self.culled_resource_count,"transientSlotCount":self.transient_slot_count})
serde_json::json!({"compiledId":id,"graphId":self.graph_id,"revision":self.revision,"schemaVersion":self.schema_version,"nodeCount":self.node_count,"executionCount":self.executions.len(),"physicalPassCount":self.render_passes.len(),"resourceCount":self.resources.len(),"culledNodeCount":self.culled_node_count,"culledResourceCount":self.culled_resource_count,"transientSlotCount":self.transient_slot_count})
}
}
+196 -32
View File
@@ -182,6 +182,7 @@ pub struct RuntimeAllocationPlan {
pub struct RuntimePlan {
pub allocations: RuntimeAllocationPlan,
pub executions: Vec<RuntimeExecution>,
pub render_passes: Vec<PhysicalRenderPass>,
pub instance_traversal: Option<InstanceTraversalPlan>,
pub surface: RuntimeSurfaceContract,
}
@@ -365,17 +366,9 @@ fn invalid(message: impl Into<String>, path: impl Into<String>) -> GraphError {
error("GRAPH_RUNTIME_PLAN_INVALID", message, path)
}
fn valid_pipeline_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 64
&& name.bytes().enumerate().all(|(i, byte)| {
byte == b'_' || byte.is_ascii_alphanumeric() && (i > 0 || byte.is_ascii_alphabetic())
})
}
fn execution_supported(key: &str) -> bool {
contract(key).is_some_and(|contract| {
contract.fullscreen_policy.is_some() || matches!(key, "pipeline" | "frame_out")
contract.fullscreen_policy.is_some() || contract.is_raster_draw() || key == "frame_out"
})
}
@@ -524,13 +517,13 @@ fn validate_fullscreen_execution(
if output.socket != "color" {
return Err(invalid("fullscreen outputs mismatch", path("outputs")));
}
let ExecutionKind::Render {
color_attachments,
depth_stencil: None,
} = &execution.kind
else {
if !matches!(execution.kind, ExecutionKind::Fullscreen) {
return Err(invalid("fullscreen render kind mismatch", path("kind")));
};
}
let (color_attachments, depth_stencil) = super::compiler::execution_attachments(execution);
if depth_stencil.is_some() {
return Err(invalid("fullscreen depth mismatch", path("kind")));
}
let [attachment] = color_attachments.as_slice() else {
return Err(invalid("fullscreen attachment mismatch", path("kind")));
};
@@ -749,7 +742,7 @@ fn validate_pipeline_resolve(
.filter(|access| matches!(access.mode, AccessMode::ColorResolve { .. }))
.count()
== 1;
if producer.executor.key != "pipeline"
if !contract(&producer.executor.key).is_some_and(Contract::is_raster_draw)
|| producer.original_node_index != *producer_node_index
|| !exact_output
|| !matches!(&source.origin,
@@ -808,6 +801,123 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
"schemaVersion",
));
}
let mut expected_execution = 0usize;
for (pass_index, pass) in graph.render_passes.iter().enumerate() {
if pass.executions.is_empty() {
return Err(invalid(
"physical pass is empty",
format!("renderPasses[{pass_index}]"),
));
}
for &member in &pass.executions {
if member as usize != expected_execution || expected_execution >= graph.executions.len()
{
return Err(invalid(
"physical passes must partition logical executions in order",
format!("renderPasses[{pass_index}].executions"),
));
}
expected_execution += 1;
}
let singleton = pass.executions.len() == 1;
let kinds_valid = match &pass.kind {
PhysicalRenderPassKind::Surface => {
singleton
&& matches!(
graph.executions[pass.executions[0] as usize].kind,
ExecutionKind::FrameOut { .. }
)
}
PhysicalRenderPassKind::Texture {
color_attachments,
depth_stencil,
} => {
pass.executions.iter().all(|&member| {
!matches!(
graph.executions[member as usize].kind,
ExecutionKind::FrameOut { .. }
)
}) && (singleton
|| pass.executions.iter().all(|&member| {
matches!(
graph.executions[member as usize].kind,
ExecutionKind::RasterDraw
)
}))
&& color_attachments.iter().all(|attachment| {
(attachment.resource as usize) < graph.resources.len()
&& attachment
.resolve_target
.is_none_or(|resource| (resource as usize) < graph.resources.len())
})
&& depth_stencil.as_ref().is_none_or(|attachment| {
(attachment.resource as usize) < graph.resources.len()
})
}
};
if !kinds_valid {
return Err(invalid(
"physical pass kind or attachment is invalid",
format!("renderPasses[{pass_index}]"),
));
}
}
if expected_execution != graph.executions.len() {
return Err(invalid(
"physical passes omit logical executions",
"renderPasses",
));
}
for (resource_index, resource) in graph.resources.iter().enumerate() {
if let ResourcePlan::Texture { family, .. } | ResourcePlan::TextureSource { family, .. } =
resource.plan
{
if family as usize >= graph.texture_families.len() {
return Err(invalid(
"texture family is out of bounds",
format!("resources[{resource_index}].plan.family"),
));
}
}
}
let canonical_passes = super::compiler::build_render_passes(
&graph.executions,
&graph.resources,
&graph.texture_families,
);
if graph.render_passes != canonical_passes {
return Err(invalid(
"physical render pass plan is not canonical",
"renderPasses",
));
}
for (pass_index, pass) in graph.render_passes.iter().enumerate() {
if !matches!(pass.kind, PhysicalRenderPassKind::Texture { .. }) {
continue;
}
let mut previous = None;
for &member in &pass.executions {
let execution = &graph.executions[member as usize];
let NormalizedParameters::Raster { draw_order, .. } = &execution.parameters else {
previous = None;
continue;
};
let key = (*draw_order, execution.original_node_index);
if previous.is_some_and(|value| value > key) {
return Err(invalid(
"raster pass members are not in canonical draw order",
format!("renderPasses[{pass_index}].executions"),
));
}
previous = Some(key);
}
}
let execution_pass: Vec<u32> = graph
.render_passes
.iter()
.enumerate()
.flat_map(|(pass, value)| value.executions.iter().map(move |_| pass as u32))
.collect();
let mut producers = vec![None; graph.resources.len()];
let mut uses = vec![BTreeSet::new(); graph.resources.len()];
@@ -878,7 +988,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
format!("executions[{i}]"),
)
})?
.insert(i as u32);
.insert(execution_pass[i]);
}
}
for (ri, resource) in graph.resources.iter().enumerate() {
@@ -910,6 +1020,52 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
format!("executions[{i}].inputs"),
));
}
let consumer_contract =
contract(&execution.executor.key).expect("supported executor has contract");
let is_attachment = consumer_contract
.inputs
.iter()
.find(|candidate| candidate.name == input.socket)
.is_some_and(|candidate| {
matches!(
candidate.role,
InputRole::ColorTarget { .. } | InputRole::DepthTarget
)
});
let predecessor = &graph.executions[producer as usize];
if is_attachment
&& matches!(execution.kind, ExecutionKind::RasterDraw)
&& matches!(predecessor.kind, ExecutionKind::RasterDraw)
{
let NormalizedParameters::Raster {
draw_order: predecessor_order,
..
} = predecessor.parameters
else {
return Err(invalid(
"raster predecessor parameters mismatch",
format!("executions[{producer}].parameters"),
));
};
let NormalizedParameters::Raster {
draw_order: consumer_order,
..
} = execution.parameters
else {
return Err(invalid(
"raster execution parameters mismatch",
format!("executions[{i}].parameters"),
));
};
if (predecessor_order, predecessor.original_node_index)
> (consumer_order, execution.original_node_index)
{
return Err(invalid(
"raster attachment predecessors must have canonical draw order",
format!("executions[{i}].parameters.drawOrder"),
));
}
}
}
}
}
@@ -1004,8 +1160,9 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
.count()
== 1
})
&& owner_executions[0].executor.key == "pipeline"
&& owner_executions[0].executor.version == 4
&& contract(&owner_executions[0].executor.key)
.is_some_and(Contract::is_raster_draw)
&& owner_executions[0].executor.version == 1
&& graph
.executions
.iter()
@@ -1013,7 +1170,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
.filter(|input| input.resource == source)
.count()
== 1
&& contract("pipeline")
&& contract(&owner_executions[0].executor.key)
.and_then(|contract| contract.inputs.get(*input_ordinal as usize))
.is_some_and(|input| {
input.name == *socket
@@ -1242,7 +1399,11 @@ fn validate_instance_traversal(graph: &CompiledGraph) -> Result<(), GraphError>
.executions
.iter()
.enumerate()
.filter_map(|(i, e)| (e.executor.key == "pipeline").then_some(i as u32))
.filter_map(|(i, e)| {
contract(&e.executor.key)
.is_some_and(Contract::is_raster_draw)
.then_some(i as u32)
})
.collect();
let Some(plan) = &graph.instance_traversal else {
return if pipeline_indices.is_empty() {
@@ -1552,7 +1713,7 @@ pub fn prepare_runtime_plan(
for (i, execution) in graph.executions.iter().enumerate() {
let path = format!("executions[{i}]");
match execution.executor.key.as_str() {
"pipeline" => {}
key if contract(key).is_some_and(Contract::is_raster_draw) => {}
_ if contract(&execution.executor.key)
.is_some_and(|contract| contract.fullscreen_policy.is_some()) => {}
"frame_out" => {
@@ -1587,11 +1748,11 @@ pub fn prepare_runtime_plan(
validate_instance_traversal(graph)?;
for (i, execution) in graph.executions.iter().enumerate() {
if execution.executor.key != "pipeline" {
if !contract(&execution.executor.key).is_some_and(Contract::is_raster_draw) {
continue;
}
let NormalizedParameters::Pipeline {
pipeline,
let NormalizedParameters::Raster {
draw_order: _,
clear_depth,
clear_color,
..
@@ -1602,8 +1763,7 @@ pub fn prepare_runtime_plan(
format!("executions[{i}].parameters"),
));
};
if !valid_pipeline_name(pipeline)
|| !clear_depth.is_finite()
if !clear_depth.is_finite()
|| !(0.0..=1.0).contains(clear_depth)
|| clear_color.iter().any(|value| !value.is_finite())
{
@@ -1612,15 +1772,18 @@ pub fn prepare_runtime_plan(
format!("executions[{i}].parameters"),
));
}
let ExecutionKind::Render {
color_attachments,
depth_stencil: Some(depth_attachment),
} = &execution.kind
else {
if !matches!(execution.kind, ExecutionKind::RasterDraw) {
return Err(invalid(
"pipeline render kind mismatch",
format!("executions[{i}].kind"),
));
}
let (color_attachments, depth_stencil) = super::compiler::execution_attachments(execution);
let Some(depth_attachment) = depth_stencil.as_ref() else {
return Err(invalid(
"pipeline depth attachment missing",
format!("executions[{i}].kind"),
));
};
let [color_attachment] = color_attachments.as_slice() else {
return Err(invalid(
@@ -2307,6 +2470,7 @@ pub fn prepare_runtime_plan(
resource_allocations,
},
executions,
render_passes: graph.render_passes.clone(),
instance_traversal: graph.instance_traversal.clone(),
surface,
})
+452 -28
View File
@@ -49,8 +49,8 @@ pub(crate) fn full_cull_graph() -> Value {
node("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
node("class", "and", 2, json!({}),
json!({"inputs":[input("bits","bit0")[0].clone(),input("visible","value")[0].clone()]})),
node("pipeline", "pipeline", 4,
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
node("pipeline", "gltf_standard", 1,
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")})),
node("frame", "frame_out", 3,
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,
@@ -62,7 +62,16 @@ pub(crate) fn full_cull_graph() -> Value {
#[test]
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
assert_eq!(contract("mesh").unwrap().version, 2);
assert_eq!(contract("pipeline").unwrap().version, 4);
assert!(contract("pipeline").is_none());
for key in [
"ground_plane",
"gltf_standard",
"gltf_standard_double_sided",
] {
let contract = contract(key).unwrap();
assert_eq!(contract.version, 1);
assert!(contract.is_raster_draw());
}
assert_eq!(
contract("mesh")
.unwrap()
@@ -76,7 +85,7 @@ fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
("localAabb", SemanticType::LocalAabb)
]
);
let predicate = contract("pipeline")
let predicate = contract("gltf_standard")
.unwrap()
.inputs
.iter()
@@ -348,7 +357,7 @@ fn pipeline_predicate_defaults_true_and_expression_edges_are_validated() {
.unwrap();
assert!(matches!(
pipeline.parameters,
NormalizedParameters::Pipeline {
NormalizedParameters::Raster {
predicate_default: true,
..
}
@@ -359,6 +368,93 @@ fn pipeline_predicate_defaults_true_and_expression_edges_are_validated() {
assert_eq!(compile_value(cycle).unwrap_err().code, "GRAPH_CYCLE");
}
#[test]
fn raster_executors_preserve_identity_and_signed_draw_order() {
for (key, draw_order) in [
("ground_plane", i32::MIN),
("gltf_standard", 0),
("gltf_standard_double_sided", i32::MAX),
] {
let mut graph = full_cull_graph();
graph["nodes"][8]["executor"] = json!({"key":key,"version":1});
graph["nodes"][8]["parameters"]["drawOrder"] = json!(draw_order);
let compiled = compile_value(graph).unwrap();
let execution = compiled
.executions
.iter()
.find(|e| e.id == "pipeline")
.unwrap();
assert_eq!(execution.executor.key, key);
assert_eq!(execution.executor.version, 1);
assert!(matches!(
execution.parameters,
NormalizedParameters::Raster { draw_order: value, .. } if value == draw_order
));
}
}
#[test]
fn raster_executors_reject_removed_pipeline_parameter() {
let mut graph = full_cull_graph();
graph["nodes"][8]["parameters"]["pipeline"] = json!("gltf_standard");
let error = compile_value(graph).unwrap_err();
assert_eq!(error.code, "GRAPH_PARAMETERS_INVALID");
assert_eq!(error.details["path"], "nodes[8].parameters");
}
#[test]
fn removed_generic_pipeline_executor_is_unknown() {
let mut graph = full_cull_graph();
graph["nodes"][8]["executor"] = json!({"key":"pipeline","version":4});
let error = compile_value(graph).unwrap_err();
assert_eq!(error.code, "GRAPH_UNKNOWN_EXECUTOR");
assert_eq!(error.details["path"], "nodes[8].executor.key");
}
#[test]
fn sibling_raster_writers_form_one_ordered_physical_pass() {
let mut graph = full_cull_graph();
let nodes = graph["nodes"].as_array_mut().unwrap();
nodes.insert(
9,
node(
"sibling",
"ground_plane",
1,
json!({"drawOrder":1,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
json!({"mesh":input("mesh","mesh"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")}),
),
);
nodes.insert(10, texture("composite", "rgba16_float"));
nodes.insert(
11,
node(
"combine",
"bloom_composite",
1,
json!({"intensity":1.0}),
json!({"source":input("pipeline","color"),"bloom":input("sibling","color"),"colorTarget":input("composite","texture")}),
),
);
nodes[12]["inputs"]["color"] = input("combine", "color");
let compiled = compile_value(graph).unwrap();
assert_eq!(
compiled
.executions
.iter()
.map(|execution| execution.id.as_str())
.collect::<Vec<_>>(),
["pipeline", "sibling", "combine", "frame"]
);
assert_eq!(compiled.render_passes[0].executions, [0, 1]);
assert!(matches!(
compiled.render_passes[0].kind,
PhysicalRenderPassKind::Texture { .. }
));
assert_eq!(compiled.texture_families[0].versions.len(), 2);
assert_eq!(compiled.texture_families[1].versions.len(), 2);
}
#[test]
fn expression_provenance_rejects_cross_mesh_values() {
let mut graph = full_cull_graph();
@@ -375,11 +471,11 @@ fn expression_provenance_rejects_cross_mesh_values() {
fn implicit_pipeline_graph() -> Value {
json!({ "schemaVersion": 3, "graphId": "implicit", "revision": 1, "nodes": [
node("mesh", "mesh", 2, json!({}), json!({})),
node("first", "pipeline", 4,
json!({"pipeline":"ground_plane","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
node("first", "ground_plane", 1,
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
json!({"mesh":input("mesh","mesh")})),
node("second", "pipeline", 4,
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
node("second", "gltf_standard", 1,
json!({"drawOrder":1,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
json!({"mesh":input("mesh","mesh"),"colorTarget":input("first","color"),"depthTarget":input("first","depth")})),
node("frame", "frame_out", 3,
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]}),
@@ -387,10 +483,294 @@ fn implicit_pipeline_graph() -> Value {
]})
}
fn three_raster_graph() -> Value {
let mut graph = full_cull_graph();
let nodes = graph["nodes"].as_array_mut().unwrap();
nodes[8]["executor"] = json!({"key":"ground_plane","version":1});
nodes[8]["parameters"]["drawOrder"] = json!(20);
nodes.insert(
9,
node(
"standard",
"gltf_standard",
1,
json!({"drawOrder":10,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[1,0,0,1],"predicateDefault":true}),
json!({"mesh":input("mesh","mesh"),"colorTarget":input("pipeline","color"),"depthTarget":input("pipeline","depth")}),
),
);
nodes.insert(
10,
node(
"double",
"gltf_standard_double_sided",
1,
json!({"drawOrder":0,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":0.5,"clearColor":[0,1,0,1],"predicateDefault":true}),
json!({"mesh":input("mesh","mesh"),"colorTarget":input("standard","color"),"depthTarget":input("standard","depth")}),
),
);
nodes[11]["inputs"]["color"] = input("double", "color");
graph
}
fn direct_raster_graph(frame_source: &str) -> Value {
let mut graph = three_raster_graph();
for node in [8, 9, 10] {
graph["nodes"][node]["inputs"]["colorTarget"] = input("color", "texture");
graph["nodes"][node]["inputs"]["depthTarget"] = input("depth", "texture");
}
graph["nodes"][11]["inputs"]["color"] = input(frame_source, "color");
graph
}
#[test]
fn direct_raster_outputs_all_observe_the_terminal_cohort() {
let mut terminal = None;
for source in ["pipeline", "standard", "double"] {
let graph = compile_value(direct_raster_graph(source)).unwrap();
assert_eq!(
graph
.executions
.iter()
.map(|execution| execution.id.as_str())
.collect::<Vec<_>>(),
["double", "standard", "pipeline", "frame"]
);
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
let frame_color = match graph.executions[3].kind {
ExecutionKind::FrameOut { color } => color,
_ => panic!("expected frame output"),
};
assert_eq!(*terminal.get_or_insert(frame_color), frame_color);
let producers: Vec<_> = graph.executions[..3]
.iter()
.flat_map(|execution| execution.outputs.iter().map(|output| output.resource))
.collect();
assert_eq!(
producers
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len(),
6
);
assert!(validate_activatable(&graph).is_ok());
}
}
#[test]
fn equal_draw_order_uses_authored_node_order() {
let mut value = direct_raster_graph("pipeline");
for node in [8, 9, 10] {
value["nodes"][node]["parameters"]["drawOrder"] = json!(7);
}
let graph = compile_value(value).unwrap();
assert_eq!(
graph.executions[..3]
.iter()
.map(|execution| execution.id.as_str())
.collect::<Vec<_>>(),
["pipeline", "standard", "double"]
);
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
}
#[test]
fn dead_fullscreen_attachment_writer_of_observed_raster_output_stays_dead() {
let mut value = direct_raster_graph("pipeline");
value["nodes"].as_array_mut().unwrap().insert(
11,
node(
"dead_writer",
"fullscreen_copy",
1,
json!({}),
json!({
"source": input("color", "texture"),
"colorTarget": input("pipeline", "color")
}),
),
);
let graph = compile_value(value).unwrap();
assert!(!graph
.executions
.iter()
.any(|execution| execution.id == "dead_writer"));
}
#[test]
fn dead_cyclic_fullscreen_reader_does_not_become_live_through_war() {
let mut value = direct_raster_graph("pipeline");
value["nodes"].as_array_mut().unwrap().insert(
11,
node(
"dead_reader",
"fullscreen_copy",
1,
json!({}),
json!({
"source": input("pipeline", "color"),
"colorTarget": input("dead_reader", "color")
}),
),
);
let graph = compile_value(value).unwrap();
assert!(!graph
.executions
.iter()
.any(|execution| execution.id == "dead_reader"));
}
#[test]
fn explicit_raster_chain_compiles_to_one_physical_pass() {
let graph = compile_value(three_raster_graph()).unwrap();
assert_eq!(graph.executions.len(), 4);
assert!(graph.executions[..3]
.iter()
.all(|execution| matches!(execution.kind, ExecutionKind::RasterDraw)));
assert_eq!(graph.render_passes.len(), 2);
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
assert_eq!(graph.render_passes[1].executions, [3]);
let PhysicalRenderPassKind::Texture {
color_attachments,
depth_stencil: Some(depth),
} = &graph.render_passes[0].kind
else {
panic!("expected raster pass")
};
let first = execution_attachments(&graph.executions[0]);
let final_attachments = execution_attachments(&graph.executions[2]);
assert_eq!(color_attachments[0].load, first.0[0].load);
assert_eq!(
color_attachments[0].resource,
final_attachments.0[0].resource
);
assert_eq!(color_attachments[0].store, final_attachments.0[0].store);
assert_eq!(
depth.resource,
final_attachments.1.as_ref().unwrap().resource
);
assert_eq!(depth.store, final_attachments.1.as_ref().unwrap().store);
for execution in &graph.executions[1..3] {
let (color, depth) = execution_attachments(execution);
assert_eq!(color[0].load, NormalizedColorLoad::Load);
assert_eq!(depth.unwrap().load, NormalizedDepthLoad::Load);
}
for socket in ["color", "depth"] {
let resources: Vec<_> = graph.executions[..3]
.iter()
.map(|execution| {
execution
.outputs
.iter()
.find(|output| output.socket == socket)
.unwrap()
.resource
})
.collect();
let family = |resource| match graph.resources[resource as usize].plan {
ResourcePlan::Texture { family, .. } => family,
_ => panic!("expected texture version"),
};
let allocation = |resource| match graph.resources[resource as usize].plan {
ResourcePlan::Texture { allocation, .. } => allocation,
_ => panic!("expected texture version"),
};
assert!(resources
.iter()
.all(|&resource| family(resource) == family(resources[0])));
assert!(resources
.iter()
.all(|&resource| allocation(resource) == allocation(resources[0])));
assert!(resources[..2]
.iter()
.all(|&resource| graph.resources[resource as usize].lifetime
== Some(Lifetime {
first_use: 0,
last_use: 0
})));
}
let traversal = graph.instance_traversal.as_ref().unwrap();
assert_eq!(
traversal
.pipelines
.iter()
.map(|pipeline| (pipeline.execution, pipeline.ordinal))
.collect::<Vec<_>>(),
[(0, 0), (1, 1), (2, 2)]
);
assert!(validate_activatable(&graph).is_ok());
assert_eq!(
serde_json::to_value(&graph).unwrap(),
serde_json::to_value(compile_value(three_raster_graph()).unwrap()).unwrap()
);
}
#[test]
fn final_msaa_resolve_stays_on_merged_raster_boundary() {
let mut value = three_raster_graph();
set_sample_count(&mut value["nodes"][0], 4);
set_sample_count(&mut value["nodes"][1], 4);
let graph = compile_value(value).unwrap();
assert_eq!(graph.render_passes[0].executions, [0, 1, 2]);
let PhysicalRenderPassKind::Texture {
color_attachments, ..
} = &graph.render_passes[0].kind
else {
panic!("expected texture pass")
};
assert!(color_attachments[0].resolve_target.is_some());
assert!(execution_attachments(&graph.executions[0]).0[0]
.resolve_target
.is_none());
assert!(execution_attachments(&graph.executions[1]).0[0]
.resolve_target
.is_none());
assert!(validate_activatable(&graph).is_ok());
}
#[test]
fn runtime_rejects_noncanonical_physical_passes() {
let graph = compile_value(three_raster_graph()).unwrap();
for mutate in [
|graph: &mut CompiledGraph| {
graph.render_passes[0].executions.pop();
},
|graph: &mut CompiledGraph| {
graph.render_passes[0].executions.push(1);
},
|graph: &mut CompiledGraph| {
graph.render_passes[0].executions.swap(0, 1);
},
] {
let mut invalid = graph.clone();
mutate(&mut invalid);
assert_runtime_plan_invalid(&invalid);
}
let mut wrong_resource = graph.clone();
let first_color = execution_attachments(&wrong_resource.executions[0]).0[0].resource;
let PhysicalRenderPassKind::Texture {
color_attachments, ..
} = &mut wrong_resource.render_passes[0].kind
else {
panic!()
};
color_attachments[0].resource = first_color;
assert_runtime_plan_invalid(&wrong_resource);
let mut wrong_lifetime = graph;
let intermediate = wrong_lifetime.executions[1].outputs[0].resource;
wrong_lifetime.resources[intermediate as usize].lifetime = Some(Lifetime {
first_use: 1,
last_use: 1,
});
assert_runtime_plan_invalid(&wrong_lifetime);
}
#[test]
fn contract_v4_declares_strict_default_policies() {
let pipeline = contract("pipeline").unwrap();
assert_eq!(pipeline.version, 4);
let pipeline = contract("gltf_standard").unwrap();
assert_eq!(pipeline.version, 1);
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
assert_eq!(
pipeline.inputs[1].default_policy,
@@ -445,13 +825,9 @@ fn disconnected_targets_have_tagged_roots_and_clear_version_zero() {
.iter()
.find(|e| e.id == "first")
.unwrap();
let ExecutionKind::Render {
color_attachments,
depth_stencil: Some(depth),
} = &first.kind
else {
panic!()
};
assert!(matches!(first.kind, ExecutionKind::RasterDraw));
let (color_attachments, depth_stencil) = execution_attachments(first);
let depth = depth_stencil.unwrap();
assert!(matches!(
color_attachments[0].load,
NormalizedColorLoad::Clear { .. }
@@ -468,13 +844,9 @@ fn implicit_chain_is_deterministic_and_loads_successors() {
serde_json::to_value(&b).unwrap()
);
let second = a.executions.iter().find(|e| e.id == "second").unwrap();
let ExecutionKind::Render {
color_attachments,
depth_stencil: Some(depth),
} = &second.kind
else {
panic!()
};
assert!(matches!(second.kind, ExecutionKind::RasterDraw));
let (color_attachments, depth_stencil) = execution_attachments(second);
let depth = depth_stencil.unwrap();
assert_eq!(color_attachments[0].load, NormalizedColorLoad::Load);
assert_eq!(depth.load, NormalizedDepthLoad::Load);
}
@@ -586,7 +958,8 @@ fn descriptor_dependency_cycle_keeps_graph_cycle_priority() {
.as_object_mut()
.unwrap()
.remove("depthTarget");
assert_eq!(compile_value(graph).unwrap_err().code, "GRAPH_CYCLE");
// The backwards authored attachment is normalized by draw order.
assert!(compile_value(graph).is_ok());
}
#[test]
@@ -595,8 +968,8 @@ fn known_attachment_error_precedes_cycle() {
graph["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
graph["nodes"][8]["inputs"]["depthTarget"] = input("pipeline", "depth");
let error = compile_value(graph).unwrap_err();
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
assert_eq!(error.details["path"], "nodes[8].inputs.colorTarget");
assert_eq!(error.code, "GRAPH_ATTACHMENT_LINEAGE_INVALID");
assert_eq!(error.details["path"], "nodes[8].inputs");
}
fn self_targeting_copy(source_format: &str) -> Value {
@@ -633,6 +1006,57 @@ fn known_invalid_fullscreen_source_precedes_target_cycle() {
);
}
#[test]
fn fullscreen_output_is_a_valid_raster_attachment_root() {
let mut graph = full_cull_graph();
let nodes = graph["nodes"].as_array_mut().unwrap();
nodes.insert(9, texture("copy_target", "rgba16_float"));
nodes.insert(
10,
node(
"copy",
"fullscreen_copy",
1,
json!({}),
json!({
"source": input("pipeline", "color"),
"colorTarget": input("copy_target", "texture")
}),
),
);
nodes.insert(
11,
node(
"later",
"ground_plane",
1,
json!({"drawOrder":1,"depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
json!({
"mesh": input("mesh", "mesh"),
"colorTarget": input("copy", "color"),
"depthTarget": input("pipeline", "depth")
}),
),
);
nodes[12]["inputs"]["color"] = input("later", "color");
let compiled = compile_value(graph).unwrap();
assert_eq!(
compiled
.executions
.iter()
.map(|execution| execution.id.as_str())
.collect::<Vec<_>>(),
["pipeline", "copy", "later", "frame"]
);
assert_eq!(compiled.render_passes.len(), 4);
assert!(compiled
.render_passes
.iter()
.all(|pass| pass.executions.len() == 1));
assert!(validate_activatable(&compiled).is_ok());
}
#[test]
fn known_invalid_fullscreen_target_precedes_source_cycle() {
let mut graph = implicit_pipeline_graph();
+152 -165
View File
@@ -54,7 +54,7 @@ pub(crate) fn encode_compiled<T: Scene>(
indirect_commands: &wgpu::Buffer,
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
) -> Result<(), &'static str> {
use crate::render_graph::{ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
use crate::render_graph::{NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
let view = |resource: u32| -> Result<&wgpu::TextureView, &'static str> {
let a = active
.runtime
@@ -71,176 +71,163 @@ pub(crate) fn encode_compiled<T: Scene>(
.map(|s| &s.view)
.ok_or(" allocation out of bounds")
};
for prepared in &active.executions {
match prepared {
PreparedExecution::Fullscreen {
execution,
frame_out,
bind_group,
pipeline,
..
} => {
let execution = active
for physical in &active.runtime.render_passes {
let first = *physical.executions.first().ok_or("empty physical pass")? as usize;
let last = *physical.executions.last().ok_or("empty physical pass")? as usize;
let label = if first == last {
active
.graph
.executions
.get(first)
.ok_or("execution out of bounds")?
.id
.clone()
} else {
format!(
"{}..{}",
active
.graph
.executions
.get(*execution)
.ok_or(" execution out of bounds")?;
let (target, operations) = if *frame_out {
let ExecutionKind::FrameOut { .. } = execution.kind else {
return Err("frame_out kind mismatch");
};
(
surface,
wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
)
} else {
let ExecutionKind::Render {
color_attachments, ..
} = &execution.kind
else {
return Err("fullscreen is not render");
};
let color = color_attachments
.first()
.ok_or("fullscreen target missing")?;
(
view(color.resource)?,
wgpu::Operations {
load: match color.load {
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
NormalizedColorLoad::Clear { value } => {
wgpu::LoadOp::Clear(wgpu::Color {
r: value[0],
g: value[1],
b: value[2],
a: value[3],
})
}
.get(first)
.ok_or("execution out of bounds")?
.id,
active
.graph
.executions
.get(last)
.ok_or("execution out of bounds")?
.id
)
};
let (colors, depth) = match &physical.kind {
crate::render_graph::PhysicalRenderPassKind::Surface => (
vec![Some(wgpu::RenderPassColorAttachment {
view: surface,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
None,
),
crate::render_graph::PhysicalRenderPassKind::Texture {
color_attachments,
depth_stencil,
} => {
let colors = color_attachments
.iter()
.map(|color| {
Ok(Some(wgpu::RenderPassColorAttachment {
view: view(color.resource)?,
depth_slice: None,
resolve_target: color.resolve_target.map(view).transpose()?,
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 {
wgpu::StoreOp::Discard
},
},
)
};
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&execution.id),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
depth_slice: None,
resolve_target: None,
ops: operations,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: profile
.as_deref_mut()
.and_then(|p| p.render_writes(&execution.id)),
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}))
})
.collect::<Result<Vec<_>, &'static str>>()?;
let depth = depth_stencil
.as_ref()
.map(|depth| -> Result<_, &'static str> {
Ok(wgpu::RenderPassDepthStencilAttachment {
view: view(depth.resource)?,
depth_ops: Some(wgpu::Operations {
load: match depth.load {
NormalizedDepthLoad::Load => wgpu::LoadOp::Load,
NormalizedDepthLoad::Clear { value } => {
wgpu::LoadOp::Clear(value)
}
},
store: if depth.store == StoreOp::Store {
wgpu::StoreOp::Store
} else {
wgpu::StoreOp::Discard
},
}),
stencil_ops: None,
})
})
.transpose()?;
(colors, depth)
}
PreparedExecution::Pipeline {
execution,
base,
predicate_ordinal,
variant,
} => {
let execution = active
.graph
.executions
.get(*execution)
.ok_or(" execution out of bounds")?;
let ExecutionKind::Render {
color_attachments,
depth_stencil,
} = &execution.kind
else {
return Err("pipeline is not render");
};
let color = color_attachments.first().ok_or("pipeline color missing")?;
let depth = depth_stencil.as_ref().ok_or("pipeline depth missing")?;
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&execution.id),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
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, &[]);
};
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&label),
color_attachments: &colors,
depth_stencil_attachment: depth,
occlusion_query_set: None,
timestamp_writes: profile.as_deref_mut().and_then(|p| p.render_writes(&label)),
});
for &member in &physical.executions {
match active
.executions
.get(member as usize)
.ok_or("prepared execution out of bounds")?
{
PreparedExecution::Fullscreen {
bind_group,
pipeline,
..
} => {
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}
if let (Some(p), Some(n), Some(u), Some(t), Some(ix), Some(inst)) = (
&gpu.positions.buffer,
&gpu.normals.buffer,
&gpu.uvs.buffer,
&gpu.tangents.buffer,
&gpu.indices.buffer,
&gpu.instances.buffer,
) {
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), &[]);
PreparedExecution::Pipeline {
base,
predicate_ordinal,
variant,
..
} => {
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)) = (
&gpu.positions.buffer,
&gpu.normals.buffer,
&gpu.uvs.buffer,
&gpu.tangents.buffer,
&gpu.indices.buffer,
&gpu.instances.buffer,
) {
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,
),
);
}
}
}
+15 -24
View File
@@ -815,14 +815,11 @@ struct GpuTextureSlot {
enum PreparedExecution {
Pipeline {
execution: usize,
base: crate::render_data::PipelineKey,
predicate_ordinal: u32,
variant: wgpu::RenderPipeline,
},
Fullscreen {
execution: usize,
frame_out: bool,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
_uniform: wgpu::Buffer,
@@ -1687,17 +1684,17 @@ impl<T: Scene + 'static> Renderer<T> {
.iter()
.enumerate()
.map(|(index, execution)| {
let NormalizedParameters::Pipeline { pipeline, .. } = &execution.parameters else {
let NormalizedParameters::Raster { .. } = &execution.parameters else {
return Ok(None);
};
self.resources
.find_pipeline(pipeline)
.find_pipeline(&execution.executor.key)
.map(Some)
.ok_or_else(|| {
GraphError::at(
"GRAPH_EXECUTION_UNSUPPORTED",
format!("pipeline '{pipeline}' is not registered"),
format!("executions[{index}].parameters.pipeline"),
format!("pipeline '{}' is not registered", execution.executor.key),
format!("executions[{index}].executor.key"),
)
})
})
@@ -1952,12 +1949,11 @@ impl<T: Scene + 'static> Renderer<T> {
let target_format = if frame_out {
runtime.surface.format
} else {
let ExecutionKind::Render {
color_attachments, ..
} = &execution.kind
else {
if !matches!(execution.kind, ExecutionKind::Fullscreen) {
return Err(fail("fullscreen execution is not render"));
};
}
let (color_attachments, _) =
crate::render_graph::execution_attachments(execution);
let target = color_attachments
.first()
.ok_or_else(|| fail("fullscreen target missing"))?
@@ -2045,21 +2041,17 @@ impl<T: Scene + 'static> Renderer<T> {
],
});
executions.push(PreparedExecution::Fullscreen {
execution: index,
frame_out,
bind_group,
pipeline,
_uniform: uniform,
});
}
"pipeline" => {
let ExecutionKind::Render {
color_attachments,
depth_stencil,
} = &execution.kind
else {
_ if contract.is_raster_draw() => {
if !matches!(execution.kind, ExecutionKind::RasterDraw) {
return Err(fail("pipeline is not render"));
};
}
let (color_attachments, depth_stencil) =
crate::render_graph::execution_attachments(execution);
let color = color_attachments
.first()
.ok_or_else(|| fail("pipeline color missing"))?;
@@ -2098,8 +2090,8 @@ impl<T: Scene + 'static> Renderer<T> {
.ok_or_else(|| fail("depth allocation invalid"))
})
.transpose()?;
let NormalizedParameters::Pipeline {
pipeline: _,
let NormalizedParameters::Raster {
draw_order: _,
depth_compare,
depth_write_enabled,
..
@@ -2145,7 +2137,6 @@ impl<T: Scene + 'static> Renderer<T> {
)
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
executions.push(PreparedExecution::Pipeline {
execution: index,
base,
predicate_ordinal: runtime
.instance_traversal
+8 -3
View File
@@ -4,13 +4,16 @@ use std::{
};
use wasm_bindgen::JsValue;
pub const MAX_PROFILE_PASSES: usize = crate::render_graph::MAX_EXECUTIONS;
// A frame can contain every logical execution as a singleton physical pass and
// one instance-traversal compute pass.
pub const MAX_PROFILE_PASSES: usize = crate::render_graph::MAX_EXECUTIONS + 1;
#[cfg(any(target_arch = "wasm32", test))]
const SLOT_COUNT: usize = 4;
#[cfg(any(target_arch = "wasm32", test))]
const QUERY_COUNT: u32 = (MAX_PROFILE_PASSES * 2) as u32;
#[cfg(any(target_arch = "wasm32", test))]
const RESOLVE_SIZE: u64 = QUERY_COUNT as u64 * 8;
const USED_RESOLVE_SIZE: u64 = QUERY_COUNT as u64 * 8;
const RESOLVE_SIZE: u64 = USED_RESOLVE_SIZE.next_multiple_of(wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SlotState {
@@ -396,7 +399,9 @@ mod tests {
#[test]
fn capacity_is_aligned() {
assert_eq!(RESOLVE_SIZE % wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT, 0);
assert_eq!(QUERY_COUNT, 2048)
assert!(RESOLVE_SIZE >= USED_RESOLVE_SIZE);
assert!(RESOLVE_SIZE - USED_RESOLVE_SIZE < wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT);
assert_eq!(QUERY_COUNT as usize, MAX_PROFILE_PASSES * 2)
}
#[test]
fn lazy_identity_when_disabled_unavailable_or_full() {