refactor: redesign render graph nodes

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

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-28 13:00:29 +00:00
co-authored by heaust
parent fc8c16daec
commit edc7c8ef29
23 changed files with 5229 additions and 1810 deletions
+286 -283
View File
@@ -15,15 +15,25 @@ struct TextureParameters {
texture: TextureDescriptor,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct DepthParameters {
depth_compare: CompareFunction,
depth_write_enabled: bool,
clear_depth: f32,
#[serde(deny_unknown_fields)]
struct CullParameters {
camera: ActiveCamera,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct ForwardParameters {
struct QueryParameters {
visible_predicate: TriStatePredicate,
visible_default: bool,
frustum_culled_predicate: TriStatePredicate,
frustum_culled_default: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct PipelineParameters {
pipeline: String,
depth_compare: CompareFunction,
depth_write_enabled: bool,
clear_depth: f32,
clear_color: [f64; 4],
}
#[derive(Deserialize)]
@@ -157,11 +167,12 @@ fn validate_name_grammar(s: &str, path: impl Into<String>) -> Result<(), GraphEr
}
}
pub fn mesh_predicate_matches(predicate: TriStatePredicate, flag: bool) -> bool {
pub fn mesh_predicate_matches(predicate: RuntimePredicate, flag: bool) -> bool {
match predicate {
TriStatePredicate::Any => true,
TriStatePredicate::RequiredTrue => flag,
TriStatePredicate::RequiredFalse => !flag,
RuntimePredicate::Any => true,
RuntimePredicate::RequiredTrue => flag,
RuntimePredicate::RequiredFalse => !flag,
RuntimePredicate::Never => false,
}
}
@@ -349,12 +360,12 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
}};
}
Ok(match node.executor.key.as_str() {
"surface_target" => empty!(NormalizedParameters::SurfaceTarget),
"scene_table" => empty!(NormalizedParameters::SceneTable),
"local_aabb_buffer" => empty!(NormalizedParameters::LocalAabbBuffer),
"camera_frustum" => empty!(NormalizedParameters::CameraFrustum),
"visibility_flags" => empty!(NormalizedParameters::VisibilityFlags),
"frustum_cull" => empty!(NormalizedParameters::FrustumCull),
"mesh" => empty!(NormalizedParameters::Mesh),
"frustum_cull" => {
let p: CullParameters =
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
NormalizedParameters::FrustumCull { camera: p.camera }
}
"fullscreen_copy" => empty!(NormalizedParameters::FullscreenCopy),
"tone_map" => {
let p: ToneMapParameters =
@@ -402,8 +413,8 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
strength: range(p.strength, 0.0, 16.0, format!("{base}.strength"))?,
}
}
"present" => empty!(NormalizedParameters::Present),
"texture_spec" => {
"frame_out" => empty!(NormalizedParameters::FrameOut),
"texture" => {
let p: TextureParameters =
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
if matches!(
@@ -416,100 +427,86 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
format!("{base}.residency"),
));
}
NormalizedParameters::TextureSpec {
let unsupported = |suffix: &str| {
error(
"GRAPH_UNSUPPORTED_FEATURE",
"texture feature is unsupported",
format!("{base}.texture.{suffix}"),
)
};
if p.texture.dimension != TextureDimension::D2 {
return Err(unsupported("dimension"));
}
if p.texture.mip_level_count != 1 {
return Err(unsupported("mipLevelCount"));
}
if p.texture.sample_count != 1 {
return Err(unsupported("sampleCount"));
}
let depth_or_array_layers = match &p.texture.extent {
TextureExtent::Absolute {
depth_or_array_layers,
..
}
| TextureExtent::SurfaceRelative {
depth_or_array_layers,
..
} => *depth_or_array_layers,
};
if depth_or_array_layers != 1 {
return Err(unsupported("extent.depthOrArrayLayers"));
}
NormalizedParameters::Texture {
residency: p.residency,
texture: normalize_texture(p.texture, &base)?,
descriptor: normalize_texture(p.texture, &base)?,
}
}
"mesh_query" => {
let object = node.parameters.as_object().ok_or_else(|| {
error(
"GRAPH_PARAMETERS_INVALID",
"parameters must be an object",
base.clone(),
)
})?;
if object.len() != 1 || !object.contains_key("filters") {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
"mesh query parameters must contain only filters",
base.clone(),
));
}
let filters = object["filters"].as_array().ok_or_else(|| {
error(
"GRAPH_PARAMETERS_INVALID",
"filters must be an array",
format!("{base}.filters"),
)
})?;
let mut found = [None, None];
for (j, value) in filters.iter().enumerate() {
let filter = value.as_object().ok_or_else(|| {
error(
"GRAPH_PARAMETERS_INVALID",
"filter must be an object",
format!("{base}.filters[{j}]"),
)
})?;
if filter.len() != 2
|| !filter.contains_key("flag")
|| !filter.contains_key("predicate")
{
return Err(error(
"GRAPH_PARAMETERS_INVALID",
"filter must contain flag and predicate",
format!("{base}.filters[{j}]"),
));
}
let flag: MeshFlag =
serde_json::from_value(filter["flag"].clone()).map_err(|e| {
error(
"GRAPH_PARAMETERS_INVALID",
&e.to_string(),
format!("{base}.filters[{j}].flag"),
)
})?;
let predicate: TriStatePredicate =
serde_json::from_value(filter["predicate"].clone()).map_err(|e| {
error(
"GRAPH_PARAMETERS_INVALID",
&e.to_string(),
format!("{base}.filters[{j}].predicate"),
)
})?;
let index = if flag == MeshFlag::IsVisible { 0 } else { 1 };
if found[index].replace(predicate).is_some() {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
"duplicate mesh flag",
format!("{base}.filters[{j}].flag"),
));
}
}
if found.iter().any(Option::is_none) {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
"both mesh flags are required",
format!("{base}.filters"),
));
let p: QueryParameters =
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
let fold = |predicate, default, linked| match (predicate, linked, default) {
(TriStatePredicate::Any, _, _) => RuntimePredicate::Any,
(TriStatePredicate::RequiredTrue, true, _) => RuntimePredicate::RequiredTrue,
(TriStatePredicate::RequiredFalse, true, _) => RuntimePredicate::RequiredFalse,
(TriStatePredicate::RequiredTrue, false, true)
| (TriStatePredicate::RequiredFalse, false, false) => RuntimePredicate::Any,
_ => RuntimePredicate::Never,
};
let mut visible = fold(
p.visible_predicate,
p.visible_default,
node.inputs.contains_key("isVisible"),
);
let mut culled = fold(
p.frustum_culled_predicate,
p.frustum_culled_default,
node.inputs.contains_key("isFrustumCulled"),
);
if visible == RuntimePredicate::Never || culled == RuntimePredicate::Never {
visible = RuntimePredicate::Never;
culled = RuntimePredicate::Never;
}
NormalizedParameters::MeshQuery {
filters: [
NormalizedMeshFilter {
flag: MeshFlag::IsVisible,
predicate: found[0].unwrap(),
},
NormalizedMeshFilter {
flag: MeshFlag::IsFrustumCulled,
predicate: found[1].unwrap(),
},
],
visible_predicate: visible,
frustum_culled_predicate: culled,
}
}
"depth_stencil_config" => {
let p: DepthParameters =
"pipeline_registry" => empty!(NormalizedParameters::PipelineRegistry),
"pipeline" => {
let p: PipelineParameters =
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",
@@ -517,17 +514,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
format!("{base}.clearDepth"),
));
}
NormalizedParameters::DepthStencilConfig {
config: NormalizedDepthStencil {
depth_compare: p.depth_compare,
depth_write_enabled: p.depth_write_enabled,
clear_depth: p.clear_depth,
},
}
}
"legacy_forward" => {
let p: ForwardParameters =
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
if p.clear_color.iter().any(|x| !x.is_finite()) {
return Err(error(
"GRAPH_PARAMETERS_INVALID",
@@ -535,7 +521,11 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
format!("{base}.clearColor"),
));
}
NormalizedParameters::LegacyForward {
NormalizedParameters::Pipeline {
pipeline: p.pipeline,
depth_compare: p.depth_compare,
depth_write_enabled: p.depth_write_enabled,
clear_depth: p.clear_depth,
clear_color: p.clear_color,
}
}
@@ -569,19 +559,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
));
}
}
if graph
.nodes
.iter()
.filter(|n| n.executor.key == "present")
.count()
> 64
{
return Err(error(
"GRAPH_LIMIT_EXCEEDED",
"present count exceeds 64",
"nodes",
));
}
if graph.schema_version != 2 {
return Err(GraphError::new(
"GRAPH_SCHEMA_UNSUPPORTED",
@@ -673,8 +650,21 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
.enumerate()
.map(|(i, n)| decode(n, i))
.collect::<Result<_, _>>()?;
if graph
.nodes
.iter()
.filter(|node| node.executor.key == "frame_out" && node.state == NodeState::Enabled)
.count()
!= 1
{
return Err(error(
"GRAPH_EXECUTION_UNSUPPORTED",
"exactly one frame_out is required",
"nodes",
));
}
for (i, n) in graph.nodes.iter().enumerate() {
if n.state != NodeState::Enabled {
if n.state != NodeState::Enabled && n.executor.key != "frame_out" {
return Err(error(
"GRAPH_NODE_STATE_INVALID",
"muted nodes are unsupported",
@@ -710,12 +700,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
for (i, n) in graph.nodes.iter().enumerate() {
for input in contracts[i].inputs {
let inactive = matches!(&params[i], NormalizedParameters::MeshQuery { filters } if filters.iter().any(|f| f.flag.input_socket() == input.name && f.predicate == TriStatePredicate::Any));
let inactive = matches!(&params[i], NormalizedParameters::MeshQuery { visible_predicate, frustum_culled_predicate } if (input.name == "isVisible" && matches!(visible_predicate, RuntimePredicate::Any | RuntimePredicate::Never)) || (input.name == "isFrustumCulled" && matches!(frustum_culled_predicate, RuntimePredicate::Any | RuntimePredicate::Never)));
if !n.inputs.contains_key(input.name) {
if input.cardinality == InputCardinality::RequiredOne
|| (!inactive
&& matches!(params[i], NormalizedParameters::MeshQuery { .. })
&& input.name != "scene")
&& input.name != "mesh")
{
return Err(error(
"GRAPH_SOCKET_CARDINALITY",
@@ -730,7 +720,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let mut bound: Vec<BTreeMap<&str, BoundInput>> = vec![BTreeMap::new(); graph.nodes.len()];
for (i, n) in graph.nodes.iter().enumerate() {
for input in contracts[i].inputs {
let inactive = matches!(&params[i], NormalizedParameters::MeshQuery { filters } if filters.iter().any(|f| f.flag.input_socket() == input.name && f.predicate == TriStatePredicate::Any));
let inactive = matches!(&params[i], NormalizedParameters::MeshQuery { visible_predicate, frustum_culled_predicate } if (input.name == "isVisible" && matches!(visible_predicate, RuntimePredicate::Any | RuntimePredicate::Never)) || (input.name == "isFrustumCulled" && matches!(frustum_culled_predicate, RuntimePredicate::Any | RuntimePredicate::Never)));
let Some(r) = n.inputs.get(input.name) else {
continue;
};
@@ -741,10 +731,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
.enumerate()
.find(|(_, o)| o.name == r.socket)
.expect("producer sockets were globally validated");
let attachment_shape_checked_later = contracts[i].key == "legacy_forward"
&& input.name == "depthTarget"
&& out.semantic_type == SemanticType::SurfaceTarget;
if !accepts(input.accepted, out.semantic_type) && !attachment_shape_checked_later {
if !accepts(input.accepted, out.semantic_type) {
return Err(error(
"GRAPH_SOCKET_TYPE_MISMATCH",
"socket type mismatch",
@@ -782,15 +769,26 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
if !seen.insert(k.0) {
return None;
}
if contracts[k.0].outputs[k.1 as usize].semantic_type == SemanticType::SceneTable {
if contracts[k.0].key == "mesh" {
let ordinal = contracts[k.0]
.outputs
.iter()
.position(|output| output.semantic_type == SemanticType::MeshData)?;
return Some(OutputKey(k.0, ordinal as u16));
}
if contracts[k.0].outputs[k.1 as usize].semantic_type == SemanticType::MeshData {
return Some(k);
}
k = bound[k.0].get("scene")?.producer;
k = bound[k.0]
.get("mesh")
.or_else(|| bound[k.0].get("pipelineIndices"))
.or_else(|| bound[k.0].get("activation"))?
.producer;
}
};
for (i, c) in contracts.iter().enumerate() {
if c.key == "frustum_cull"
&& root(bound[i]["scene"].producer, &bound, &contracts)
&& root(bound[i]["mesh"].producer, &bound, &contracts)
!= root(bound[i]["localAabbs"].producer, &bound, &contracts)
{
return Err(error(
@@ -799,11 +797,23 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
format!("nodes[{i}].inputs.localAabbs"),
));
}
if matches!(c.key, "mesh_query" | "legacy_forward") {
let scene = root(bound[i]["scene"].producer, &bound, &contracts);
if matches!(c.key, "mesh_query" | "pipeline_registry" | "pipeline") {
let scene_socket = if c.key == "pipeline_registry" {
"pipelineIndices"
} else {
"mesh"
};
let scene = root(bound[i][scene_socket].producer, &bound, &contracts);
for (s, b) in &bound[i] {
if b.active
&& matches!(*s, "isVisible" | "isFrustumCulled" | "draws")
&& matches!(
*s,
"isVisible"
| "isFrustumCulled"
| "draws"
| "pipelineIndices"
| "activation"
)
&& root(b.producer, &bound, &contracts) != scene
{
return Err(error(
@@ -853,7 +863,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let mut stack: Vec<_> = contracts
.iter()
.enumerate()
.filter(|(_, c)| c.inherently_observable)
.filter(|(i, c)| c.inherently_observable && graph.nodes[*i].state == NodeState::Enabled)
.map(|(i, _)| i)
.collect();
while let Some(i) = stack.pop() {
@@ -862,13 +872,26 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
}
// 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.
let referenced_outputs: HashSet<_> = edges
.iter()
.filter(|edge| live.contains(&edge.to_node))
.map(|edge| OutputKey(edge.from_node, edge.producer_output_ordinal))
.collect();
let mut output_ids = BTreeMap::new();
let mut resource_meta = Vec::new();
for i in 0..graph.nodes.len() {
if live.contains(&i) {
for (o, out) in contracts[i].outputs.iter().enumerate() {
let key = OutputKey(i, o as u16);
if contracts[i].execution == ExecutionClass::Source
&& !referenced_outputs.contains(&key)
{
continue;
}
let id = resource_meta.len() as u32;
output_ids.insert(OutputKey(i, o as u16), id);
output_ids.insert(key, id);
resource_meta.push((i, o as u16, *out));
}
}
@@ -884,28 +907,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
let source = output_ids.get(&OutputKey(i, 0)).copied();
match &params[i] {
NormalizedParameters::SurfaceTarget => {
let id = families.len() as u32;
let r = source.unwrap();
source_family.insert(OutputKey(i, 0), id);
families.push(TextureFamily {
id,
key: TextureFamilyKey {
source_node: i as u32,
source_socket: 0,
},
source: TextureFamilySource::ImportedSurface { resource: r },
lifetime: Lifetime {
first_use: 0,
last_use: 0,
},
versions: vec![],
usage: vec![],
allocation: None,
aliasable: false,
});
}
NormalizedParameters::TextureSpec { residency, texture } => {
NormalizedParameters::Texture {
residency,
descriptor,
} => {
let id = families.len() as u32;
let r = source.unwrap();
source_family.insert(OutputKey(i, 0), id);
@@ -918,7 +923,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
source: TextureFamilySource::AuthoredTexture {
resource: r,
residency: *residency,
descriptor: texture.clone(),
descriptor: descriptor.clone(),
},
lifetime: Lifetime {
first_use: 0,
@@ -940,7 +945,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
continue;
}
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
"legacy_forward" => &[("colorTarget", 0), ("depthTarget", 1)],
"pipeline" => &[("colorTarget", 0), ("depthTarget", 1)],
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur" | "bloom_composite"
| "luminance_edge" => &[("colorTarget", 0)],
_ => continue,
@@ -1066,7 +1071,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
for input in contract
.inputs
.iter()
.filter(|input| matches!(input.role, InputRole::Present | InputRole::SampledTexture))
.filter(|input| matches!(input.role, InputRole::SampledTexture))
{
let key = bound[i][input.name].producer;
if !version_of.contains_key(&key) {
@@ -1103,7 +1108,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
if !live.contains(&i) {
continue;
}
if contracts[i].key == "legacy_forward" {
if contracts[i].key == "pipeline" {
if bound[i]["colorTarget"].producer == bound[i]["depthTarget"].producer
|| matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df)
{
@@ -1113,10 +1118,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
format!("nodes[{i}].inputs"),
));
}
} else if contracts[i]
.inputs
.iter()
.any(|input| matches!(input.role, InputRole::SampledTexture))
} else if contracts[i].key != "frame_out"
&& contracts[i]
.inputs
.iter()
.any(|input| matches!(input.role, InputRole::SampledTexture))
{
let hazard = contracts[i].inputs.iter().filter(|input| matches!(input.role, InputRole::SampledTexture)).any(|input| matches!((version_of.get(&bound[i][input.name].producer), version_of.get(&OutputKey(i, 0))), (Some((sf, _, _)), Some((tf, _, _))) if sf == tf));
if hazard {
@@ -1176,7 +1182,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 != "legacy_forward" {
if !live.contains(&i) || contracts[i].key != "pipeline" {
continue;
}
let (Some(&(cf, _, _)), Some(&(df, _, _))) = (
@@ -1185,33 +1191,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
) else {
continue;
};
let cd = match &families[cf as usize].source {
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
_ => None,
};
let dd = match &families[df as usize].source {
TextureFamilySource::AuthoredTexture { descriptor, .. } => descriptor,
_ => {
return Err(error(
"GRAPH_ILLEGAL_ACCESS",
"depth target must be authored",
format!("nodes[{i}].inputs.depthTarget"),
))
}
};
let TextureFamilySource::AuthoredTexture { descriptor: cd, .. } =
&families[cf as usize].source;
let TextureFamilySource::AuthoredTexture { descriptor: dd, .. } =
&families[df as usize].source;
let ok_depth = dd.dimension == TextureDimension::D2
&& dd.format == TextureFormat::Depth32Float
&& dd.sample_count == 1
&& extent_layers(&dd.extent) == 1;
let ok_color = cd.is_none_or(|d| {
d.format != TextureFormat::Depth32Float
&& d.dimension == dd.dimension
&& d.extent == dd.extent
&& d.sample_count == 1
});
let surface_ok = cd.is_some()
|| matches!(&dd.extent,NormalizedTextureExtent::SurfaceRelative{width,height,..} if *width==Ratio{numerator:1,denominator:1}&&*height==Ratio{numerator:1,denominator:1});
if !ok_depth || !ok_color || !surface_ok {
let ok_color = cd.format != TextureFormat::Depth32Float
&& cd.dimension == dd.dimension
&& cd.extent == dd.extent
&& cd.sample_count == 1;
if !ok_depth || !ok_color {
return Err(error(
"GRAPH_ILLEGAL_ACCESS",
"attachments are incompatible",
@@ -1222,6 +1214,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
for i in 0..graph.nodes.len() {
if !live.contains(&i)
|| contracts[i].key == "frame_out"
|| !contracts[i]
.inputs
.iter()
@@ -1242,19 +1235,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
};
let source_descriptor = match &families[source_family_id as usize].source {
TextureFamilySource::AuthoredTexture { descriptor, .. } => descriptor,
TextureFamilySource::ImportedSurface { .. } => {
return Err(error(
"GRAPH_ILLEGAL_ACCESS",
"copy source must be an authored texture",
format!("nodes[{i}].inputs.source"),
))
}
};
let source_ok = source_descriptor.format == TextureFormat::Rgba16Float
&& is_single_view_d2(source_descriptor);
let target_descriptor = match &families[target_family_id as usize].source {
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
TextureFamilySource::ImportedSurface { .. } => None,
};
let authored_target_ok = target_descriptor.is_some_and(|descriptor| {
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
@@ -1272,13 +1257,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
TextureFamilySource::AuthoredTexture { descriptor, .. } => {
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
}
TextureFamilySource::ImportedSurface { .. } => {
return Err(error(
"GRAPH_ILLEGAL_ACCESS",
"bloom source must be an authored texture",
format!("nodes[{i}].inputs.bloom"),
))
}
}
} else {
true
@@ -1295,7 +1273,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
&& descriptor.extent == source_descriptor.extent
})
}
"tone_map" => target_descriptor.is_none() && source_is_full_surface,
"tone_map" => target_descriptor.is_some_and(|descriptor| {
descriptor.format != TextureFormat::Depth32Float
&& descriptor.format != TextureFormat::R32Float
&& is_single_view_d2(descriptor)
&& descriptor.extent == source_descriptor.extent
}),
"bloom_extract" => authored_target_ok,
"bloom_blur" | "luminance_edge" => authored_target_ok && target_matches_source,
"bloom_composite" => authored_target_ok && target_matches_source && bloom_input_ok,
@@ -1310,30 +1293,29 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
}
// Initialization and presentation legality are later than attachment compatibility.
// Frame output must consume an initialized, produced, filterable color texture.
for (i, contract) in contracts.iter().enumerate() {
if !live.contains(&i) || contract.key != "present" {
if !live.contains(&i) || contract.key != "frame_out" {
continue;
}
let key = bound[i]["surface"].producer;
let key = bound[i]["color"].producer;
let Some(&(family, _, _)) = version_of.get(&key) else {
if !matches!(resolved.get(&key), Some(ResolvedTransition::Cyclic)) {
return Err(error(
"GRAPH_UNINITIALIZED_RESOURCE",
"present source is not produced",
format!("nodes[{i}].inputs.surface"),
"frame output source is not produced",
format!("nodes[{i}].inputs.color"),
));
}
continue;
};
if !matches!(
families[family as usize].source,
TextureFamilySource::ImportedSurface { .. }
) {
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
&families[family as usize].source;
if !is_filterable_frame_color(descriptor) {
return Err(error(
"GRAPH_ILLEGAL_ACCESS",
"offscreen textures cannot be presented",
format!("nodes[{i}].inputs.surface"),
"frame output requires a filterable single-view d2 color texture",
format!("nodes[{i}].inputs.color"),
));
}
}
@@ -1463,17 +1445,18 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
for (i, o, out) in resource_meta {
let key = OutputKey(i, o);
let id = output_ids[&key];
let scene = || output_ids[&root(bound[i]["scene"].producer, &bound, &contracts).unwrap()];
let mesh = || output_ids[&root(key, &bound, &contracts).unwrap()];
let plan = match out.semantic_type {
SemanticType::SurfaceTarget => ResourcePlan::SurfaceTarget {
family: source_family[&key],
},
SemanticType::TextureSpec => {
if let NormalizedParameters::TextureSpec { residency, texture } = &params[i] {
ResourcePlan::TextureSpec {
SemanticType::Texture if matches!(params[i], NormalizedParameters::Texture { .. }) => {
if let NormalizedParameters::Texture {
residency,
descriptor,
} = &params[i]
{
ResourcePlan::TextureSource {
family: source_family[&key],
residency: *residency,
descriptor: texture.clone(),
descriptor: descriptor.clone(),
}
} else {
unreachable!()
@@ -1490,27 +1473,20 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
allocation: None,
}
}
SemanticType::SceneTable => ResourcePlan::SceneTable,
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { scene: scene() },
SemanticType::CameraFrustum => ResourcePlan::CameraFrustum,
SemanticType::MeshData => ResourcePlan::MeshData,
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { mesh: mesh() },
SemanticType::BooleanFlagBuffer => {
if let OutputMetadata::BooleanFlag { flag } = out.metadata {
ResourcePlan::BooleanFlagBuffer {
scene: scene(),
flag,
}
} else {
unreachable!()
}
}
SemanticType::DrawStream => ResourcePlan::DrawStream { scene: scene() },
SemanticType::DepthStencilConfig => {
if let NormalizedParameters::DepthStencilConfig { config } = &params[i] {
ResourcePlan::DepthStencilConfig { config: *config }
ResourcePlan::BooleanFlagBuffer { mesh: mesh(), flag }
} else {
unreachable!()
}
}
SemanticType::PipelineIndexStream => ResourcePlan::PipelineIndexStream { mesh: mesh() },
SemanticType::PipelineActivation => ResourcePlan::PipelineActivation {
pipeline_indices: output_ids[&bound[i]["pipelineIndices"].producer],
},
SemanticType::DrawStream => ResourcePlan::DrawStream { mesh: mesh() },
};
resources.push(CompiledResource {
original_node_index: i as u32,
@@ -1557,9 +1533,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
let kind = match contracts[i].key {
"frustum_cull" => {
for (s, m) in [
("scene", AccessMode::StorageRead),
("mesh", AccessMode::StorageRead),
("localAabbs", AccessMode::StorageRead),
("frustum", AccessMode::UniformRead),
] {
accesses.push(CompiledAccess {
socket: s.into(),
@@ -1569,7 +1544,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
let r = output_ids[&OutputKey(i, 0)];
accesses.push(CompiledAccess {
socket: "flags".into(),
socket: "isFrustumCulled".into(),
resource: r,
mode: AccessMode::StorageWrite {
full_overwrite: true,
@@ -1580,7 +1555,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
}
}
"mesh_query" => {
for s in ["scene", "isVisible", "isFrustumCulled"] {
for s in ["mesh", "isVisible", "isFrustumCulled"] {
if let Some(b) = bound[i].get(s).filter(|b| b.active) {
accesses.push(CompiledAccess {
socket: s.into(),
@@ -1600,23 +1575,38 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
work: ComputeWork::MeshQuery,
}
}
"legacy_forward" => {
"pipeline_registry" => {
accesses.push(CompiledAccess {
socket: "pipelineIndices".into(),
resource: input_resource("pipelineIndices"),
mode: AccessMode::SemanticRead,
});
ExecutionKind::CpuPreparation
}
"pipeline" => {
let color = output_ids[&OutputKey(i, 0)];
let depth = output_ids[&OutputKey(i, 1)];
let clear = match params[i] {
NormalizedParameters::LegacyForward { clear_color } => clear_color,
NormalizedParameters::Pipeline { clear_color, .. } => clear_color,
_ => unreachable!(),
};
let config_node = bound[i]["depthStencil"].producer.0;
let dc = match params[config_node] {
NormalizedParameters::DepthStencilConfig { config } => config,
let clear_depth = match &params[i] {
NormalizedParameters::Pipeline { clear_depth, .. } => *clear_depth,
_ => unreachable!(),
};
let cl = NormalizedColorLoad::Clear { value: clear };
let dl = NormalizedDepthLoad::Clear {
value: dc.clear_depth,
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
let first_depth = version_of[&OutputKey(i, 1)].1 == 0;
let cl = if first_color {
NormalizedColorLoad::Clear { value: clear }
} else {
NormalizedColorLoad::Load
};
for s in ["scene", "draws"] {
let dl = if first_depth {
NormalizedDepthLoad::Clear { value: clear_depth }
} else {
NormalizedDepthLoad::Load
};
for s in ["mesh", "draws", "activation"] {
accesses.push(CompiledAccess {
socket: s.into(),
resource: input_resource(s),
@@ -1634,7 +1624,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
location: 0,
load: cl,
store: StoreOp::Store,
full_overwrite: true,
full_overwrite: first_color,
},
});
accesses.push(CompiledAccess {
@@ -1643,7 +1633,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
mode: AccessMode::DepthAttachment {
load: dl,
store: StoreOp::Store,
full_overwrite: true,
full_overwrite: first_depth,
},
});
ExecutionKind::Render {
@@ -1698,14 +1688,14 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
depth_stencil: None,
}
}
"present" => {
let r = input_resource("surface");
"frame_out" => {
let r = input_resource("color");
accesses.push(CompiledAccess {
socket: "surface".into(),
socket: "color".into(),
resource: r,
mode: AccessMode::Present,
mode: AccessMode::SampledTexture,
});
ExecutionKind::Present { surface: r }
ExecutionKind::FrameOut { color: r }
}
_ => unreachable!(),
};
@@ -1803,13 +1793,27 @@ fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
} => *depth_or_array_layers,
}
}
fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
descriptor.dimension == TextureDimension::D2
&& descriptor.sample_count == 1
&& descriptor.mip_level_count == 1
&& extent_layers(&descriptor.extent) == 1
}
fn texture_usage(f: &TextureFamily, executions: &[CompiledExecution]) -> Vec<TextureUsage> {
pub(super) fn is_filterable_frame_color(descriptor: &NormalizedTextureDescriptor) -> bool {
is_single_view_d2(descriptor)
&& matches!(
descriptor.format,
TextureFormat::Rgba8Unorm
| TextureFormat::Rgba8UnormSrgb
| TextureFormat::Bgra8Unorm
| TextureFormat::Bgra8UnormSrgb
| TextureFormat::Rgba16Float
)
}
pub(super) fn texture_usage(
f: &TextureFamily,
executions: &[CompiledExecution],
) -> Vec<TextureUsage> {
let rs: HashSet<_> = f.versions.iter().map(|v| v.resource).collect();
let mut u = BTreeSet::new();
for e in executions {
@@ -1842,19 +1846,18 @@ fn allocate(
) -> (Vec<AllocationClass>, u32) {
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = BTreeMap::new();
for (i, f) in families.iter().enumerate() {
if let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source {
grouped
.entry(TextureCompatibilityKey {
dimension: descriptor.dimension,
format: descriptor.format,
extent: descriptor.extent.clone(),
mip_level_count: descriptor.mip_level_count,
sample_count: descriptor.sample_count,
view_formats: descriptor.view_formats.clone(),
})
.or_default()
.push(i);
}
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source;
grouped
.entry(TextureCompatibilityKey {
dimension: descriptor.dimension,
format: descriptor.format,
extent: descriptor.extent.clone(),
mip_level_count: descriptor.mip_level_count,
sample_count: descriptor.sample_count,
view_formats: descriptor.view_formats.clone(),
})
.or_default()
.push(i);
}
let mut classes = Vec::new();
let mut transient = 0;
+64 -106
View File
@@ -3,15 +3,13 @@ use super::MeshFlag;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticType {
SurfaceTarget,
TextureSpec,
MeshData,
Texture,
SceneTable,
LocalAabbBuffer,
CameraFrustum,
BooleanFlagBuffer,
PipelineIndexStream,
PipelineActivation,
DrawStream,
DepthStencilConfig,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
@@ -21,7 +19,7 @@ pub enum ExecutionClass {
CpuPreparation,
Compute,
Render,
Present,
Frame,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
@@ -48,8 +46,6 @@ pub enum InputRole {
SampledTexture,
ColorTarget { location: u32 },
DepthTarget,
Present,
Configuration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
@@ -119,48 +115,42 @@ const REQUIRED: InputCardinality = InputCardinality::RequiredOne;
const OPTIONAL: InputCardinality = InputCardinality::OptionalOne;
const NONE_IN: &[InputSocketContract] = &[];
const NONE_OUT: &[OutputSocketContract] = &[];
const SURFACE_OUT: &[OutputSocketContract] =
&[output("surface", SurfaceTarget, OutputMetadata::None)];
const SPEC_OUT: &[OutputSocketContract] = &[output("spec", TextureSpec, OutputMetadata::None)];
const SCENE_OUT: &[OutputSocketContract] = &[output("scene", SceneTable, OutputMetadata::None)];
const AABB_OUT: &[OutputSocketContract] =
&[output("localAabbs", LocalAabbBuffer, OutputMetadata::None)];
const FRUSTUM_OUT: &[OutputSocketContract] =
&[output("frustum", CameraFrustum, OutputMetadata::None)];
const VISIBLE_OUT: &[OutputSocketContract] = &[output(
"flags",
BooleanFlagBuffer,
OutputMetadata::BooleanFlag {
flag: MeshFlag::IsVisible,
},
)];
const TEXTURE_OUT: &[OutputSocketContract] = &[output("texture", Texture, OutputMetadata::None)];
const MESH_OUT: &[OutputSocketContract] = &[
output("mesh", MeshData, OutputMetadata::None),
output("localAabbs", LocalAabbBuffer, OutputMetadata::None),
output(
"isVisible",
BooleanFlagBuffer,
OutputMetadata::BooleanFlag {
flag: MeshFlag::IsVisible,
},
),
output("pipelineIndices", PipelineIndexStream, OutputMetadata::None),
];
const CULLED_OUT: &[OutputSocketContract] = &[output(
"flags",
"isFrustumCulled",
BooleanFlagBuffer,
OutputMetadata::BooleanFlag {
flag: MeshFlag::IsFrustumCulled,
},
)];
const DRAW_OUT: &[OutputSocketContract] = &[output("draws", DrawStream, OutputMetadata::None)];
const CONFIG_OUT: &[OutputSocketContract] =
&[output("config", DepthStencilConfig, OutputMetadata::None)];
const FORWARD_OUT: &[OutputSocketContract] = &[
const ACTIVATION_OUT: &[OutputSocketContract] = &[output(
"activation",
PipelineActivation,
OutputMetadata::None,
)];
const PIPELINE_OUT: &[OutputSocketContract] = &[
output("color", Texture, OutputMetadata::None),
output("depth", Texture, OutputMetadata::None),
];
const FULLSCREEN_COPY_OUT: &[OutputSocketContract] =
&[output("color", Texture, OutputMetadata::None)];
const LOCAL_IN: &[InputSocketContract] = &[input(
"scene",
TypeConstraint::Exact(SceneTable),
REQUIRED,
InputRole::SemanticRead,
)];
const VISIBILITY_IN: &[InputSocketContract] = LOCAL_IN;
const CULL_IN: &[InputSocketContract] = &[
input(
"scene",
TypeConstraint::Exact(SceneTable),
"mesh",
TypeConstraint::Exact(MeshData),
REQUIRED,
InputRole::StorageRead,
),
@@ -170,17 +160,11 @@ const CULL_IN: &[InputSocketContract] = &[
REQUIRED,
InputRole::StorageRead,
),
input(
"frustum",
TypeConstraint::Exact(CameraFrustum),
REQUIRED,
InputRole::UniformRead,
),
];
const QUERY_IN: &[InputSocketContract] = &[
input(
"scene",
TypeConstraint::Exact(SceneTable),
"mesh",
TypeConstraint::Exact(MeshData),
REQUIRED,
InputRole::StorageRead,
),
@@ -197,10 +181,16 @@ const QUERY_IN: &[InputSocketContract] = &[
InputRole::StorageRead,
),
];
const FORWARD_IN: &[InputSocketContract] = &[
const REGISTRY_IN: &[InputSocketContract] = &[input(
"pipelineIndices",
TypeConstraint::Exact(PipelineIndexStream),
REQUIRED,
InputRole::SemanticRead,
)];
const PIPELINE_IN: &[InputSocketContract] = &[
input(
"scene",
TypeConstraint::Exact(SceneTable),
"mesh",
TypeConstraint::Exact(MeshData),
REQUIRED,
InputRole::SemanticRead,
),
@@ -210,24 +200,24 @@ const FORWARD_IN: &[InputSocketContract] = &[
REQUIRED,
InputRole::IndirectRead,
),
input(
"activation",
TypeConstraint::Exact(PipelineActivation),
REQUIRED,
InputRole::SemanticRead,
),
input(
"colorTarget",
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
TypeConstraint::Exact(Texture),
REQUIRED,
InputRole::ColorTarget { location: 0 },
),
input(
"depthTarget",
TypeConstraint::OneOf(&[TextureSpec, Texture]),
TypeConstraint::Exact(Texture),
REQUIRED,
InputRole::DepthTarget,
),
input(
"depthStencil",
TypeConstraint::Exact(DepthStencilConfig),
REQUIRED,
InputRole::Configuration,
),
];
const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
input(
@@ -238,7 +228,7 @@ const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
),
input(
"colorTarget",
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
TypeConstraint::Exact(Texture),
REQUIRED,
InputRole::ColorTarget { location: 0 },
),
@@ -258,65 +248,33 @@ const BLOOM_COMPOSITE_IN: &[InputSocketContract] = &[
),
input(
"colorTarget",
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
TypeConstraint::Exact(Texture),
REQUIRED,
InputRole::ColorTarget { location: 0 },
),
];
const PRESENT_IN: &[InputSocketContract] = &[input(
"surface",
const FRAME_OUT_IN: &[InputSocketContract] = &[input(
"color",
TypeConstraint::Exact(Texture),
REQUIRED,
InputRole::Present,
InputRole::SampledTexture,
)];
pub static CONTRACTS: &[Contract] = &[
Contract {
key: "surface_target",
key: "mesh",
version: 1,
execution: ExecutionClass::Source,
inputs: NONE_IN,
outputs: SURFACE_OUT,
outputs: MESH_OUT,
inherently_observable: false,
},
Contract {
key: "texture_spec",
key: "texture",
version: 1,
execution: ExecutionClass::Source,
inputs: NONE_IN,
outputs: SPEC_OUT,
inherently_observable: false,
},
Contract {
key: "scene_table",
version: 1,
execution: ExecutionClass::Source,
inputs: NONE_IN,
outputs: SCENE_OUT,
inherently_observable: false,
},
Contract {
key: "local_aabb_buffer",
version: 1,
execution: ExecutionClass::Source,
inputs: LOCAL_IN,
outputs: AABB_OUT,
inherently_observable: false,
},
Contract {
key: "camera_frustum",
version: 1,
execution: ExecutionClass::Source,
inputs: NONE_IN,
outputs: FRUSTUM_OUT,
inherently_observable: false,
},
Contract {
key: "visibility_flags",
version: 1,
execution: ExecutionClass::Source,
inputs: VISIBILITY_IN,
outputs: VISIBLE_OUT,
outputs: TEXTURE_OUT,
inherently_observable: false,
},
Contract {
@@ -336,19 +294,19 @@ pub static CONTRACTS: &[Contract] = &[
inherently_observable: false,
},
Contract {
key: "depth_stencil_config",
key: "pipeline_registry",
version: 1,
execution: ExecutionClass::Source,
inputs: NONE_IN,
outputs: CONFIG_OUT,
execution: ExecutionClass::CpuPreparation,
inputs: REGISTRY_IN,
outputs: ACTIVATION_OUT,
inherently_observable: false,
},
Contract {
key: "legacy_forward",
key: "pipeline",
version: 1,
execution: ExecutionClass::Render,
inputs: FORWARD_IN,
outputs: FORWARD_OUT,
inputs: PIPELINE_IN,
outputs: PIPELINE_OUT,
inherently_observable: false,
},
Contract {
@@ -400,10 +358,10 @@ pub static CONTRACTS: &[Contract] = &[
inherently_observable: false,
},
Contract {
key: "present",
key: "frame_out",
version: 1,
execution: ExecutionClass::Present,
inputs: PRESENT_IN,
execution: ExecutionClass::Frame,
inputs: FRAME_OUT_IN,
outputs: NONE_OUT,
inherently_observable: true,
},
+1 -1
View File
@@ -49,4 +49,4 @@ impl GraphError {
}
#[cfg(test)]
mod tests;
pub(crate) mod tests;
+33 -46
View File
@@ -33,10 +33,7 @@ pub struct CompiledResource {
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResourcePlan {
SurfaceTarget {
family: u32,
},
TextureSpec {
TextureSource {
family: u32,
residency: TextureResidency,
descriptor: NormalizedTextureDescriptor,
@@ -49,20 +46,22 @@ pub enum ResourcePlan {
stored: bool,
allocation: Option<AllocationRef>,
},
SceneTable,
MeshData,
LocalAabbBuffer {
scene: u32,
mesh: u32,
},
CameraFrustum,
BooleanFlagBuffer {
scene: u32,
mesh: u32,
flag: MeshFlag,
},
DrawStream {
scene: u32,
PipelineIndexStream {
mesh: u32,
},
DepthStencilConfig {
config: NormalizedDepthStencil,
PipelineActivation {
pipeline_indices: u32,
},
DrawStream {
mesh: u32,
},
}
@@ -104,12 +103,12 @@ pub enum ExecutionKind {
color_attachments: Vec<ColorAttachmentPlan>,
depth_stencil: Option<DepthStencilAttachmentPlan>,
},
Present {
surface: u32,
FrameOut {
color: u32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComputeWork {
FrustumCull,
@@ -184,29 +183,29 @@ pub enum AccessMode {
store: StoreOp,
full_overwrite: bool,
},
Present,
}
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum NormalizedParameters {
SurfaceTarget,
TextureSpec {
Texture {
residency: TextureResidency,
texture: NormalizedTextureDescriptor,
descriptor: NormalizedTextureDescriptor,
},
Mesh,
FrustumCull {
camera: ActiveCamera,
},
SceneTable,
LocalAabbBuffer,
CameraFrustum,
VisibilityFlags,
FrustumCull,
MeshQuery {
filters: [NormalizedMeshFilter; 2],
visible_predicate: RuntimePredicate,
frustum_culled_predicate: RuntimePredicate,
},
DepthStencilConfig {
config: NormalizedDepthStencil,
},
LegacyForward {
PipelineRegistry,
Pipeline {
pipeline: String,
depth_compare: CompareFunction,
depth_write_enabled: bool,
clear_depth: f32,
clear_color: [f64; 4],
},
FullscreenCopy,
@@ -227,22 +226,13 @@ pub enum NormalizedParameters {
LuminanceEdge {
strength: f32,
},
Present,
FrameOut,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NormalizedMeshFilter {
pub flag: MeshFlag,
pub predicate: TriStatePredicate,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NormalizedDepthStencil {
pub depth_compare: CompareFunction,
pub depth_write_enabled: bool,
pub clear_depth: f32,
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActiveCamera {
Active,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
@@ -288,9 +278,6 @@ pub struct TextureFamilyKey {
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TextureFamilySource {
ImportedSurface {
resource: u32,
},
AuthoredTexture {
resource: u32,
residency: TextureResidency,
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -114,11 +114,13 @@ pub enum TriStatePredicate {
RequiredFalse,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(deny_unknown_fields)]
pub struct MeshFilter {
pub flag: MeshFlag,
pub predicate: TriStatePredicate,
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum RuntimePredicate {
Any,
RequiredTrue,
RequiredFalse,
Never,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -42,10 +42,14 @@ fn mesh_query(@builtin(global_invocation_id) id: vec3<u32>) {
if (i >= params.count) { return; }
let draw_meta = metadata[i];
var selected = true;
if (params.visible_predicate != 0u) {
if (params.visible_predicate == 3u) {
selected = false;
} else if (params.visible_predicate != 0u) {
selected = matches(authored_visible[i], params.visible_predicate);
}
if (params.frustum_predicate != 0u) {
if (selected && params.frustum_predicate == 3u) {
selected = false;
} else if (selected && params.frustum_predicate != 0u) {
selected = selected && matches(frustum_flags[i], params.frustum_predicate);
}
commands[i] = Command(draw_meta.index_count, select(0u, 1u, selected), draw_meta.first_index, draw_meta.base_vertex, 0u);
+2 -2
View File
@@ -1,3 +1,3 @@
mod legacy_forward;
mod pipeline;
pub(super) use legacy_forward::{encode_compiled, encode_immediate};
pub(super) use pipeline::{encode_compiled, encode_immediate};
@@ -56,25 +56,8 @@ pub(crate) fn encode_compiled<T: Scene>(
materials: &MaterialResources,
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
) -> Result<(), &'static str> {
use crate::render_graph::{
ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, ResourcePlan, StoreOp,
};
use crate::render_graph::{ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
let view = |resource: u32| -> Result<&wgpu::TextureView, &'static str> {
let is_surface = active
.graph
.resources
.get(resource as usize)
.is_some_and(|resource| {
matches!(
resource.plan,
ResourcePlan::SurfaceTarget { family }
| ResourcePlan::Texture { family, .. }
if family == active.runtime.allocations.surface_family
)
});
if is_surface {
return Ok(surface);
}
let a = active
.runtime
.allocations
@@ -93,15 +76,16 @@ pub(crate) fn encode_compiled<T: Scene>(
for (execution_index, prepared) in active.executions.iter().enumerate() {
let profile_id = &active.graph.executions[execution_index].id;
match prepared {
PreparedExecution::PipelineRegistry => {}
PreparedExecution::FrustumCull => {
gpu.encode_frustum_cull(encoder, profile.as_deref_mut(), profile_id);
}
PreparedExecution::MeshQuery => {
gpu.encode_mesh_query(encoder, profile.as_deref_mut(), profile_id);
}
PreparedExecution::Present => {}
PreparedExecution::Fullscreen {
execution,
frame_out,
bind_group,
pipeline,
..
@@ -111,22 +95,30 @@ pub(crate) fn encode_compiled<T: Scene>(
.executions
.get(*execution)
.ok_or(" execution out of bounds")?;
let ExecutionKind::Render {
color_attachments, ..
} = &execution.kind
else {
return Err("fullscreen is not render");
};
let color = color_attachments
.first()
.ok_or("fullscreen target 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: None,
ops: wgpu::Operations {
let (target, operations) = if *frame_out {
let ExecutionKind::FrameOut { .. } = execution.kind else {
return Err("frame_out kind mismatch");
};
(
surface,
wgpu::Operations {
load: wgpu::LoadOp::Load,
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 } => {
@@ -144,6 +136,15 @@ pub(crate) fn encode_compiled<T: Scene>(
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,
@@ -155,9 +156,10 @@ pub(crate) fn encode_compiled<T: Scene>(
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}
PreparedExecution::LegacyForward {
PreparedExecution::Pipeline {
execution,
variants,
base,
variant,
} => {
let execution = active
.graph
@@ -169,10 +171,10 @@ pub(crate) fn encode_compiled<T: Scene>(
depth_stencil,
} = &execution.kind
else {
return Err("legacy forward is not render");
return Err("pipeline is not render");
};
let color = color_attachments.first().ok_or("legacy color missing")?;
let depth = depth_stencil.as_ref().ok_or("legacy depth missing")?;
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 {
@@ -235,16 +237,14 @@ pub(crate) fn encode_compiled<T: Scene>(
pass.set_vertex_buffer(4, t.slice(..));
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
for draw in &gpu.draws {
if draw.pipeline != *base {
continue;
}
let slot = draw.instances.start as u64;
let start = slot
* std::mem::size_of::<crate::renderer::gpu_scene::GpuInstance>() as u64;
pass.set_vertex_buffer(3, inst.slice(start..start + 112));
let key = variants
.iter()
.find(|(base, _)| *base == draw.pipeline)
.map(|x| &x.1)
.ok_or("pipeline variant missing")?;
pass.set_pipeline(key);
pass.set_pipeline(variant);
if pipelines.requires_material(draw.pipeline) {
pass.set_bind_group(2, materials.group(draw.material), &[]);
}
@@ -274,7 +274,7 @@ pub(crate) fn encode_immediate<T: Scene>(
profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render pass"),
label: Some("Immediate pipeline pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
depth_slice: None,
view: color,
@@ -298,7 +298,7 @@ pub(crate) fn encode_immediate<T: Scene>(
stencil_ops: None,
}),
occlusion_query_set: None,
timestamp_writes: profile.and_then(|p| p.render_writes("immediate.forward")),
timestamp_writes: profile.and_then(|p| p.render_writes("immediate.pipeline")),
});
encode_scene(&mut pass, scene, gpu, pipelines, materials);
}
+30 -15
View File
@@ -85,8 +85,8 @@ impl GpuScenePlan {
self::GpuScenePlan::build_with_query(
data,
crate::render_graph::MeshQueryRuntimeKey {
visible: crate::render_graph::TriStatePredicate::RequiredTrue,
frustum_culled: crate::render_graph::TriStatePredicate::Any,
visible: crate::render_graph::RuntimePredicate::RequiredTrue,
frustum_culled: crate::render_graph::RuntimePredicate::Any,
},
)
}
@@ -277,7 +277,6 @@ pub struct BufferSlot {
#[derive(Default)]
pub struct GpuSceneCache {
revision: Option<u64>,
query: Option<crate::render_graph::MeshQueryRuntimeKey>,
pub positions: BufferSlot,
pub normals: BufferSlot,
pub uvs: BufferSlot,
@@ -310,11 +309,12 @@ struct CullingParams {
_pad: u32,
}
fn predicate_code(value: crate::render_graph::TriStatePredicate) -> u32 {
fn predicate_code(value: crate::render_graph::RuntimePredicate) -> u32 {
match value {
crate::render_graph::TriStatePredicate::Any => 0,
crate::render_graph::TriStatePredicate::RequiredTrue => 1,
crate::render_graph::TriStatePredicate::RequiredFalse => 2,
crate::render_graph::RuntimePredicate::Any => 0,
crate::render_graph::RuntimePredicate::RequiredTrue => 1,
crate::render_graph::RuntimePredicate::RequiredFalse => 2,
crate::render_graph::RuntimePredicate::Never => 3,
}
}
@@ -330,8 +330,8 @@ impl GpuSceneCache {
queue,
data,
crate::render_graph::MeshQueryRuntimeKey {
visible: crate::render_graph::TriStatePredicate::RequiredTrue,
frustum_culled: crate::render_graph::TriStatePredicate::Any,
visible: crate::render_graph::RuntimePredicate::RequiredTrue,
frustum_culled: crate::render_graph::RuntimePredicate::Any,
},
)
}
@@ -343,14 +343,13 @@ impl GpuSceneCache {
data: &SceneFramePlan,
query: crate::render_graph::MeshQueryRuntimeKey,
) -> Result<(), String> {
if self.revision == Some(data.revision) && self.query == Some(query) {
if self.revision == Some(data.revision) {
return Ok(());
}
let plan = GpuScenePlan::build_with_query(data, query).map_err(str::to_owned)?;
if plan.draws.is_empty() {
self.draws.clear();
self.revision = Some(data.revision);
self.query = Some(query);
return Ok(());
}
let maximum = device.limits().max_buffer_size;
@@ -485,7 +484,6 @@ impl GpuSceneCache {
self.draws = plan.draws;
self.rebuild_compute(device)?;
self.revision = Some(data.revision);
self.query = Some(query);
Ok(())
}
@@ -642,11 +640,28 @@ mod tests {
#[test]
fn mesh_query_source_guards_optional_flag_buffer_reads() {
let source = include_str!("culling.wgsl");
let visible_guard = source.find("if (params.visible_predicate != 0u)").unwrap();
let visible_never = source.find("if (params.visible_predicate == 3u)").unwrap();
let visible_guard = source
.find("else if (params.visible_predicate != 0u)")
.unwrap();
let visible_load = source.find("matches(authored_visible[i]").unwrap();
let frustum_guard = source.find("if (params.frustum_predicate != 0u)").unwrap();
let frustum_never = source
.find("if (selected && params.frustum_predicate == 3u)")
.unwrap();
let frustum_guard = source
.find("else if (selected && params.frustum_predicate != 0u)")
.unwrap();
let frustum_load = source.find("matches(frustum_flags[i]").unwrap();
assert!(visible_guard < visible_load && frustum_guard < frustum_load);
assert!(visible_never < visible_guard && visible_guard < visible_load);
assert!(frustum_never < frustum_guard && frustum_guard < frustum_load);
assert!(
visible_load < frustum_load,
"visible rejection must precede the frustum load"
);
assert!(
source.contains("predicate == 0u ||"),
"predicate zero is handled without a load by the guards"
);
for binding in 0..=6 {
assert!(source.contains(&format!("@binding({binding})")));
}
+155 -103
View File
@@ -35,17 +35,19 @@ struct GpuTextureSlot {
enum PreparedExecution {
FrustumCull,
MeshQuery,
LegacyForward {
PipelineRegistry,
Pipeline {
execution: usize,
variants: Vec<(crate::render_data::PipelineKey, wgpu::RenderPipeline)>,
base: crate::render_data::PipelineKey,
variant: wgpu::RenderPipeline,
},
Fullscreen {
execution: usize,
frame_out: bool,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
_uniform: wgpu::Buffer,
},
Present,
}
struct ActiveCompiledGraph {
@@ -81,7 +83,10 @@ fn resolve_culling_frustum(
query: crate::render_graph::MeshQueryRuntimeKey,
read: impl FnOnce() -> Option<Result<[[f32; 4]; 6], crate::camera::FrustumError>>,
) -> Result<Option<[[f32; 4]; 6]>, crate::render_graph::GraphError> {
if query.frustum_culled == crate::render_graph::TriStatePredicate::Any {
if matches!(
query.frustum_culled,
crate::render_graph::RuntimePredicate::Any | crate::render_graph::RuntimePredicate::Never
) {
return Ok(None);
}
match read() {
@@ -177,18 +182,25 @@ fn resolve_switch_request(
#[cfg(test)]
mod switch_request_tests {
fn valid_compile_graph(graph_id: &str, revision: u64) -> Vec<u8> {
let mut graph = crate::render_graph::tests::full_cull_graph();
graph["graphId"] = serde_json::json!(graph_id);
graph["revision"] = serde_json::json!(revision);
serde_json::to_vec(&graph).unwrap()
}
use super::*;
fn query(visible: crate::render_graph::TriStatePredicate) -> UploadGraph {
fn query(visible: crate::render_graph::RuntimePredicate) -> UploadGraph {
UploadGraph::Compiled(crate::render_graph::MeshQueryRuntimeKey {
visible,
frustum_culled: crate::render_graph::TriStatePredicate::Any,
frustum_culled: crate::render_graph::RuntimePredicate::Any,
})
}
#[test]
fn upload_selection_follows_the_graph_rendered_for_the_commit_frame() {
use crate::render_graph::TriStatePredicate::{Any, RequiredFalse, RequiredTrue};
use crate::render_graph::RuntimePredicate::{Any, RequiredFalse, RequiredTrue};
let selected =
|pending, active| upload_query_for_render(pending, active).map(|query| query.visible);
assert_eq!(
@@ -214,7 +226,7 @@ mod switch_request_tests {
#[test]
fn frustum_preflight_skips_any_and_distinguishes_missing_from_invalid() {
use crate::render_graph::TriStatePredicate::{Any, RequiredFalse, RequiredTrue};
use crate::render_graph::RuntimePredicate::{Any, RequiredFalse, RequiredTrue};
let query = |frustum_culled| crate::render_graph::MeshQueryRuntimeKey {
visible: RequiredTrue,
frustum_culled,
@@ -245,8 +257,10 @@ mod switch_request_tests {
#[test]
fn resolves_at_command_boundary_before_gpu_work() {
let mut registry = crate::render_graph::Registry::default();
let bytes = br#"{"schemaVersion":2,"graphId":"switch","revision":1,"nodes":[]}"#;
let (id, _) = registry.compile(bytes).unwrap();
let mut graph = crate::render_graph::tests::full_cull_graph();
graph["graphId"] = serde_json::json!("switch");
let bytes = serde_json::to_vec(&graph).unwrap();
let (id, _) = registry.compile(&bytes).unwrap();
let active = "existing_graph";
let pending: Option<&str> = None;
assert_eq!(
@@ -279,9 +293,7 @@ mod switch_request_tests {
#[test]
fn resize_restart_snapshot_remains_bound_to_its_immutable_registry_revision() {
let mut registry = crate::render_graph::Registry::default();
let (id, _) = registry
.compile(br#"{"schemaVersion":2,"graphId":"resize","revision":1,"nodes":[]}"#)
.unwrap();
let (id, _) = registry.compile(&valid_compile_graph("resize", 1)).unwrap();
let revision_one = registry.get(id).unwrap().clone();
let in_flight = InFlightPreparation {
token: 1,
@@ -289,9 +301,7 @@ mod switch_request_tests {
purpose: PreparationPurpose::Resize,
graph: revision_one,
};
let (revision_two_id, _) = registry
.compile(br#"{"schemaVersion":2,"graphId":"resize","revision":2,"nodes":[]}"#)
.unwrap();
let (revision_two_id, _) = registry.compile(&valid_compile_graph("resize", 2)).unwrap();
let original = registry.get(id).unwrap();
let revision_two = registry.get(revision_two_id).unwrap();
assert_eq!(in_flight.graph.revision, 1);
@@ -728,6 +738,26 @@ impl<T: Scene + 'static> Renderer<T> {
) -> Result<ActiveCompiledGraph, crate::render_graph::GraphError> {
use crate::render_graph::*;
let fail = |message| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", message);
let resolved_pipelines = graph
.executions
.iter()
.enumerate()
.map(|(index, execution)| {
let NormalizedParameters::Pipeline { pipeline, .. } = &execution.parameters else {
return Ok(None);
};
self.resources
.find_pipeline(pipeline)
.map(Some)
.ok_or_else(|| {
GraphError::at(
"GRAPH_EXECUTION_UNSUPPORTED",
format!("pipeline '{pipeline}' is not registered"),
format!("executions[{index}].parameters.pipeline"),
)
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut textures = Vec::with_capacity(runtime.allocations.classes.len());
for class in &runtime.allocations.classes {
let mut gpu_class = Vec::with_capacity(class.slots.len());
@@ -845,37 +875,53 @@ impl<T: Scene + 'static> Renderer<T> {
match execution.executor.key.as_str() {
"frustum_cull" => executions.push(PreparedExecution::FrustumCull),
"mesh_query" => executions.push(PreparedExecution::MeshQuery),
"present" => executions.push(PreparedExecution::Present),
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
"frame_out" | "fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
| "bloom_composite" | "luminance_edge" => {
let sampled: Vec<_> = execution
.accesses
.iter()
.filter(|a| matches!(a.mode, AccessMode::SampledTexture))
.map(|a| a.resource)
.collect();
let source = *sampled
.first()
.ok_or_else(|| fail("fullscreen source missing"))?;
let second = *sampled.get(1).unwrap_or(&source);
let values: [f32; 8] = match execution.parameters {
NormalizedParameters::ToneMap { exposure } => {
[exposure, 0., 0., 0., 0., 0., 0., 0.]
let frame_out = execution.executor.key == "frame_out";
let (source, second) = if frame_out {
let ExecutionKind::FrameOut { color } = execution.kind else {
return Err(fail("frame_out kind mismatch"));
};
(color, color)
} else {
match execution.inputs.as_slice() {
[source, _color_target] => (source.resource, source.resource),
[source, bloom, _color_target]
if execution.executor.key == "bloom_composite" =>
{
(source.resource, bloom.resource)
}
_ => return Err(fail("fullscreen inputs mismatch")),
}
NormalizedParameters::BloomExtract { threshold, knee } => {
[threshold, knee, 0., 0., 0., 0., 0., 0.]
}
NormalizedParameters::BloomBlur { direction, radius } => {
[direction[0], direction[1], radius, 0., 0., 0., 0., 0.]
}
NormalizedParameters::BloomComposite { intensity } => {
[intensity, 0., 0., 0., 0., 0., 0., 0.]
}
NormalizedParameters::LuminanceEdge { strength } => {
[strength, 0., 0., 0., 0., 0., 0., 0.]
}
_ => [0.; 8],
};
let values: [f32; 8] =
match (execution.executor.key.as_str(), &execution.parameters) {
(
"fullscreen_copy" | "frame_out",
NormalizedParameters::FullscreenCopy
| NormalizedParameters::FrameOut,
) => [0.; 8],
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
[*exposure, 0., 0., 0., 0., 0., 0., 0.]
}
(
"bloom_extract",
NormalizedParameters::BloomExtract { threshold, knee },
) => [*threshold, *knee, 0., 0., 0., 0., 0., 0.],
(
"bloom_blur",
NormalizedParameters::BloomBlur { direction, radius },
) => [direction[0], direction[1], *radius, 0., 0., 0., 0., 0.],
(
"bloom_composite",
NormalizedParameters::BloomComposite { intensity },
) => [*intensity, 0., 0., 0., 0., 0., 0., 0.],
(
"luminance_edge",
NormalizedParameters::LuminanceEdge { strength },
) => [*strength, 0., 0., 0., 0., 0., 0., 0.],
_ => return Err(fail("executor parameters mismatch")),
};
use wgpu::util::DeviceExt;
let uniform =
self.context
@@ -885,17 +931,35 @@ impl<T: Scene + 'static> Renderer<T> {
contents: bytemuck::cast_slice(&values),
usage: wgpu::BufferUsages::UNIFORM,
});
let ExecutionKind::Render {
color_attachments, ..
} = &execution.kind
else {
return Err(fail("fullscreen execution is not render"));
let target_format = if frame_out {
runtime.surface.format
} else {
let ExecutionKind::Render {
color_attachments, ..
} = &execution.kind
else {
return Err(fail("fullscreen execution is not render"));
};
let target = color_attachments
.first()
.ok_or_else(|| fail("fullscreen target missing"))?
.resource;
let a = runtime
.allocations
.resource_allocations
.get(target as usize)
.copied()
.flatten()
.ok_or_else(|| fail("fullscreen target allocation missing"))?;
runtime
.allocations
.classes
.get(a.class as usize)
.and_then(|class| class.slots.get(a.slot as usize))
.ok_or_else(|| fail("fullscreen target allocation is invalid"))?
.descriptor
.format
};
let target = color_attachments
.first()
.ok_or_else(|| fail("fullscreen target missing"))?
.resource;
let target_format = if graph.resources.get(target as usize).is_some_and(|r| matches!(r.plan, ResourcePlan::Texture { family, .. } if family == runtime.allocations.surface_family)) { runtime.surface.format } else { let a=runtime.allocations.resource_allocations[target as usize].ok_or_else(|| fail("fullscreen target allocation missing"))?; runtime.allocations.classes[a.class as usize].slots[a.slot as usize].descriptor.format };
let entry = match execution.executor.key.as_str() {
"fullscreen_copy" => "fs_copy",
"tone_map" => "fs_tone_map",
@@ -903,7 +967,8 @@ impl<T: Scene + 'static> Renderer<T> {
"bloom_blur" => "fs_bloom_blur",
"bloom_composite" => "fs_bloom_composite",
"luminance_edge" => "fs_luminance_edge",
_ => unreachable!(),
"frame_out" => "fs_copy",
_ => return Err(fail("fullscreen executor mismatch")),
};
let pipeline = self.context.device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
@@ -963,36 +1028,25 @@ impl<T: Scene + 'static> Renderer<T> {
});
executions.push(PreparedExecution::Fullscreen {
execution: index,
frame_out,
bind_group,
pipeline,
_uniform: uniform,
});
}
"legacy_forward" => {
"pipeline_registry" => executions.push(PreparedExecution::PipelineRegistry),
"pipeline" => {
let ExecutionKind::Render {
color_attachments,
depth_stencil,
} = &execution.kind
else {
return Err(fail("legacy forward is not render"));
return Err(fail("pipeline is not render"));
};
let color = color_attachments
.first()
.ok_or_else(|| fail("legacy color missing"))?;
let color_is_surface = graph
.resources
.get(color.resource as usize)
.is_some_and(|resource| {
matches!(
resource.plan,
ResourcePlan::SurfaceTarget { family }
| ResourcePlan::Texture { family, .. }
if family == runtime.allocations.surface_family
)
});
let color_format = if color_is_surface {
runtime.surface.format
} else {
.ok_or_else(|| fail("pipeline color missing"))?;
let color_format = {
let a = runtime
.allocations
.resource_allocations
@@ -1027,19 +1081,21 @@ impl<T: Scene + 'static> Renderer<T> {
.ok_or_else(|| fail("depth allocation invalid"))
})
.transpose()?;
let config = execution
.inputs
.iter()
.filter_map(|i| graph.resources.get(i.resource as usize))
.find_map(|r| {
if let ResourcePlan::DepthStencilConfig { config } = r.plan {
Some(config)
} else {
None
}
})
.ok_or_else(|| fail("depth config missing"))?;
let compare = match config.depth_compare {
let NormalizedParameters::Pipeline {
pipeline: _,
depth_compare,
depth_write_enabled,
..
} = &execution.parameters
else {
return Err(fail("pipeline parameters mismatch"));
};
let base = resolved_pipelines
.get(index)
.copied()
.flatten()
.ok_or_else(|| fail("resolved pipeline missing"))?;
let compare = match depth_compare {
CompareFunction::Never => wgpu::CompareFunction::Never,
CompareFunction::Less => wgpu::CompareFunction::Less,
CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual,
@@ -1049,25 +1105,21 @@ impl<T: Scene + 'static> Renderer<T> {
CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual,
CompareFunction::Always => wgpu::CompareFunction::Always,
};
let mut variants = Vec::new();
let bases: Vec<_> = self.resources.pipeline_keys().collect();
for base in bases {
let variant = self
.resources
.create_target_variant(
&self.context.device,
base,
color_format,
depth_format,
compare,
config.depth_write_enabled,
)
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
variants.push((base, variant));
}
executions.push(PreparedExecution::LegacyForward {
let variant = self
.resources
.create_target_variant(
&self.context.device,
base,
color_format,
depth_format,
compare,
*depth_write_enabled,
)
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
executions.push(PreparedExecution::Pipeline {
execution: index,
variants,
base,
variant,
});
}
_ => return Err(fail("unsupported prepared execution")),
-30
View File
@@ -372,36 +372,6 @@ impl PipelineLibrary {
&& self.specs[key.get() as usize].layout == self.material_layout
}
pub fn pipeline_keys(&self) -> impl Iterator<Item = PipelineKey> + '_ {
let mut keys = self
.pipeline_registry
.values()
.map(|entry| entry.0)
.collect::<Vec<_>>();
keys.sort_by_key(|key| key.get());
keys.dedup();
keys.into_iter()
}
pub fn get_or_create_target_variant(
&mut self,
device: &wgpu::Device,
base: PipelineKey,
color_format: wgpu::TextureFormat,
depth_format: Option<wgpu::TextureFormat>,
depth_compare: wgpu::CompareFunction,
depth_write: bool,
) -> Result<PipelineKey, String> {
let spec = self
.specs
.get(base.get() as usize)
.cloned()
.ok_or_else(|| "unknown base pipeline".to_owned())?;
let spec =
target_variant_spec(spec, color_format, depth_format, depth_compare, depth_write);
Ok(self.get_or_create_from_spec(device, &spec, Some("target variant")))
}
pub fn create_target_variant(
&self,
device: &wgpu::Device,