use std::cmp::Reverse; use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet}; use serde::Deserialize; use super::*; #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct Empty {} #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct TextureParameters { residency: TextureResidency, texture: TextureDescriptor, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct CullParameters { camera: ActiveCamera, } #[derive(Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct RasterParameters { depth_compare: CompareFunction, depth_write_enabled: bool, clear_depth: f32, clear_color: [f64; 4], predicate_default: bool, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] #[serde(rename_all = "camelCase")] struct FrameOutParameters { surface_format: SurfaceFormatRequest, hdr_enabled: bool, tone_mapper: ToneMapper, exposure_stops: f32, output_transfer: OutputTransfer, scale_mode: ScaleMode, filter: FrameFilter, background_color: [f32; 4], } #[derive(Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct ColorBalanceParameters { mode: ColorBalanceMode, factor: f32, lift: f32, lift_color: [f32; 4], gamma: f32, gamma_color: [f32; 4], gain: f32, gain_color: [f32; 4], offset: f32, offset_color: [f32; 4], power: f32, power_color: [f32; 4], slope: f32, slope_color: [f32; 4], } #[derive(Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct ExposureContrastParameters { exposure_stops: f32, contrast: f32, pivot: f32, factor: f32, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct SaturationParameters { saturation: f32, factor: f32, } #[derive(Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct ChannelMixerParameters { red_output: [f32; 3], green_output: [f32; 3], blue_output: [f32; 3], factor: f32, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct BloomExtractParameters { threshold: f32, knee: f32, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct BloomBlurParameters { direction: [f32; 2], radius: f32, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct BloomCompositeParameters { intensity: f32, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct LuminanceEdgeParameters { strength: f32, } fn range(value: f32, min: f32, max: f32, path: String) -> Result { if value.is_finite() && (min..=max).contains(&value) { Ok(value) } else { Err(error( "GRAPH_PARAMETERS_INVALID", &format!("value must be finite and in [{min},{max}]"), path, )) } } fn components( value: [f32; N], min: f32, max: f32, base: &str, ) -> Result<[f32; N], GraphError> { let mut result = value; for (i, component) in result.iter_mut().enumerate() { *component = range(*component, min, max, format!("{base}[{i}]"))?; } Ok(result) } fn color(value: [f32; 4], min: f32, max: f32, base: &str) -> Result<[f32; 3], GraphError> { let value = components(value, min, max, base)?; Ok([value[0], value[1], value[2]]) } #[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 { owner_node: usize, input_ordinal: u16, }, } #[derive(Clone, Copy)] struct BoundInput { producer: OutputKey, } #[derive(Clone)] struct DependencyEdge { from_node: usize, from_socket: String, producer_output_ordinal: u16, to_node: usize, to_socket: String, consumer_input_ordinal: u16, resource: NodeOutputRef, } #[derive(Clone, Copy)] struct TextureTransition { writer_node: usize, input_socket: &'static str, target: TransitionTargetKey, output: OutputKey, } #[derive(Clone, Copy)] enum ResolvedTransition { Resolved { family: u32, version: u32, target: TransitionTargetKey, }, Cyclic, } #[derive(Clone)] struct DefaultDraft { key: TransitionTargetKey, resource: u32, family: u32, owner_node: usize, input_ordinal: u16, socket: &'static str, role: CompilerTextureRole, format: TextureFormat, descriptor: DraftDescriptor, } #[derive(Clone)] enum DraftDescriptor { Deferred, Known(NormalizedTextureDescriptor), } enum SampledDescriptor<'a> { Known(&'a NormalizedTextureDescriptor), Deferred, Unproduced, } fn known_family_descriptor<'a>( family: u32, authored_families: &'a [TextureFamily], drafts: &'a [DefaultDraft], ) -> Option<&'a NormalizedTextureDescriptor> { if let Some(family) = authored_families.get(family as usize) { return Some(family_descriptor(family)); } let draft = drafts.get(family as usize - authored_families.len())?; match &draft.descriptor { DraftDescriptor::Known(descriptor) => Some(descriptor), DraftDescriptor::Deferred => None, } } fn sampled_descriptor<'a>( key: OutputKey, version_of: &HashMap, resolved: &HashMap, authored_families: &'a [TextureFamily], drafts: &'a [DefaultDraft], ) -> Result, GraphError> { if let Some(&(family, _, _)) = version_of.get(&key) { return Ok(known_family_descriptor(family, authored_families, drafts) .map(SampledDescriptor::Known) .unwrap_or(SampledDescriptor::Deferred)); } match resolved.get(&key) { Some(ResolvedTransition::Cyclic) => Ok(SampledDescriptor::Deferred), Some(ResolvedTransition::Resolved { .. }) => Err(error( "GRAPH_RESOURCE_VERSION_INVALID", "resolved texture has no version", "resources", )), None => Ok(SampledDescriptor::Unproduced), } } fn reaches( from: usize, to: usize, outgoing_edges: &[Vec], ordering_outgoing: &[Vec], edges: &[DependencyEdge], live: &HashSet, memo: &mut HashMap<(usize, usize), bool>, ) -> bool { if let Some(&answer) = memo.get(&(from, to)) { return answer; } let mut stack = vec![from]; let mut visited = HashSet::new(); let mut answer = false; while let Some(node) = stack.pop() { if !visited.insert(node) { continue; } if node == to { answer = true; break; } for &edge_index in &outgoing_edges[node] { let next = edges[edge_index].to_node; if live.contains(&next) { stack.push(next); } } for &next in &ordering_outgoing[node] { if live.contains(&next) { stack.push(next); } } } memo.insert((from, to), answer); answer } fn error(code: &'static str, message: &str, path: impl Into) -> GraphError { GraphError::at(code, message, path) } fn validate_name_length(s: &str, path: impl Into) -> Result<(), GraphError> { if s.len() > 64 { Err(error( "GRAPH_LIMIT_EXCEEDED", "identifier exceeds 64 bytes", path.into(), )) } else { Ok(()) } } fn validate_name_grammar(s: &str, path: impl Into) -> Result<(), GraphError> { if s.is_empty() || !identifier(s) { Err(error("GRAPH_INVALID_ID", "invalid identifier", path)) } else { Ok(()) } } pub fn parse_and_compile(bytes: &[u8]) -> Result { compile(super::ast::parse(bytes)?) } fn gcd(mut a: u32, mut b: u32) -> u32 { while b != 0 { (a, b) = (b, a % b); } a } fn compatible_view(a: TextureFormat, b: TextureFormat) -> bool { matches!( (a, b), (TextureFormat::Rgba8Unorm, TextureFormat::Rgba8UnormSrgb) | (TextureFormat::Rgba8UnormSrgb, TextureFormat::Rgba8Unorm) | (TextureFormat::Bgra8Unorm, TextureFormat::Bgra8UnormSrgb) | (TextureFormat::Bgra8UnormSrgb, TextureFormat::Bgra8Unorm) ) } fn normalize_texture( d: TextureDescriptor, base: &str, ) -> Result { let bad = |message: &str, suffix: &str| { error( "GRAPH_PARAMETERS_INVALID", message, format!("{base}.texture.{suffix}"), ) }; let (extent, w, h, layers, relative) = match d.extent { TextureExtent::Absolute { width, height, depth_or_array_layers, } => ( NormalizedTextureExtent::Absolute { width, height, depth_or_array_layers, }, width, height, depth_or_array_layers, false, ), TextureExtent::SurfaceRelative { mut width, mut height, depth_or_array_layers, } => { if width.numerator == 0 || width.denominator == 0 || height.numerator == 0 || height.denominator == 0 || depth_or_array_layers == 0 { return Err(bad( "extent components and ratio terms must be nonzero", "extent", )); } let g = gcd(width.numerator, width.denominator); width.numerator /= g; width.denominator /= g; let g = gcd(height.numerator, height.denominator); height.numerator /= g; height.denominator /= g; ( NormalizedTextureExtent::SurfaceRelative { width, height, depth_or_array_layers, }, 1, 1, depth_or_array_layers, true, ) } }; if w == 0 || h == 0 || layers == 0 { return Err(bad("extent components must be nonzero", "extent")); } if relative && d.dimension != TextureDimension::D2 { return Err(bad("surface-relative textures must be d2", "extent")); } if d.dimension == TextureDimension::D1 && (h != 1 || layers != 1) { return Err(bad( "d1 textures require height and layers equal to one", "extent", )); } if d.format == TextureFormat::Depth32Float && d.dimension != TextureDimension::D2 { return Err(bad("depth textures must be d2", "dimension")); } if !matches!(d.sample_count, 1 | 4) { return Err(bad("sampleCount must be 1 or 4", "sampleCount")); } if d.mip_level_count == 0 { return Err(bad("mipLevelCount must be at least one", "mipLevelCount")); } if d.sample_count == 4 && (d.dimension != TextureDimension::D2 || d.mip_level_count != 1 || layers != 1) { return Err(bad( "multisampled textures must be d2, single-mip, single-layer", "sampleCount", )); } let max_dim = w.max(h).max(if d.dimension == TextureDimension::D3 { layers } else { 1 }); let max_mips = 32 - max_dim.leading_zeros(); if !relative && d.mip_level_count > max_mips { return Err(bad( "mipLevelCount exceeds the full mip chain", "mipLevelCount", )); } let limit = if d.dimension == TextureDimension::D3 { 2048 } else { 8192 }; if w > limit || h > limit || (d.dimension == TextureDimension::D3 && layers > 2048) || (d.dimension != TextureDimension::D3 && layers > 256) { return Err(bad("texture exceeds dimension limits", "extent")); } for (j, &view) in d.view_formats.iter().enumerate() { if view == d.format || !compatible_view(d.format, view) { return Err(bad( "view format must be compatible and exclude the base format", &format!("viewFormats[{j}]"), )); } } let mut views = d.view_formats; views.sort(); views.dedup(); Ok(NormalizedTextureDescriptor { dimension: d.dimension, format: d.format, extent, mip_level_count: d.mip_level_count, sample_count: d.sample_count, view_formats: views, }) } fn decode( node: &Node, i: usize, pipelines: &PipelineDeclarations, ) -> Result { let base = format!("nodes[{i}].parameters"); let invalid = |e: serde_json::Error| error("GRAPH_PARAMETERS_INVALID", &e.to_string(), base.clone()); macro_rules! empty { ($variant:expr) => {{ serde_json::from_value::(node.parameters.clone()).map_err(invalid)?; $variant }}; } fn literal(value: &serde_json::Value, ty: SemanticType) -> Option { let floats = |value: &serde_json::Value, n: usize| -> Option> { let values = value.as_array()?; if values.len() != n { return None; } values .iter() .map(|value| value.as_f64().map(|value| value as f32)) .collect::>>() .filter(|values| values.iter().all(|value| value.is_finite())) }; let vector = |n| floats(value, n); Some(match ty { SemanticType::Bool => TypedLiteral::Bool(value.as_bool()?), SemanticType::F32 => { let value = value.as_f64()? as f32; if !value.is_finite() { return None; } TypedLiteral::F32(value) } SemanticType::U32 => TypedLiteral::U32(value.as_u64()?.try_into().ok()?), SemanticType::Vec2 => TypedLiteral::Vec2(vector(2)?.try_into().ok()?), SemanticType::Vec3 => TypedLiteral::Vec3(vector(3)?.try_into().ok()?), SemanticType::Vec4 => TypedLiteral::Vec4(vector(4)?.try_into().ok()?), SemanticType::U32x16 => TypedLiteral::U32x16( value .as_array()? .iter() .map(|v| v.as_u64()?.try_into().ok()) .collect::>>()? .try_into() .ok()?, ), SemanticType::LocalAabb => TypedLiteral::LocalAabb { min: floats(value.get("min")?, 3)?.try_into().ok()?, max: floats(value.get("max")?, 3)?.try_into().ok()?, }, ty @ (SemanticType::Mat2 | SemanticType::Mat3 | SemanticType::Mat4) => { let n = match ty { SemanticType::Mat2 => 2, SemanticType::Mat3 => 3, _ => 4, }; let columns = value.as_array()?; if columns.len() != n { return None; } let columns = columns .iter() .map(|v| floats(v, n)) .collect::>>()?; match ty { SemanticType::Mat2 => TypedLiteral::Mat2( columns .into_iter() .map(|v| v.try_into().ok()) .collect::>>()? .try_into() .ok()?, ), SemanticType::Mat3 => TypedLiteral::Mat3( columns .into_iter() .map(|v| v.try_into().ok()) .collect::>>()? .try_into() .ok()?, ), _ => TypedLiteral::Mat4( columns .into_iter() .map(|v| v.try_into().ok()) .collect::>>()? .try_into() .ok()?, ), } } SemanticType::MeshData | SemanticType::Texture => return None, }) } Ok(match node.executor.key.as_str() { "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), "color_balance" => { let p: ColorBalanceParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::ColorBalance { mode: p.mode, factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?, lift: range(p.lift, -1.0, 1.0, format!("{base}.lift"))?, lift_color: color(p.lift_color, 0.0, 4.0, &format!("{base}.liftColor"))?, gamma: range(p.gamma, 0.01, 4.0, format!("{base}.gamma"))?, gamma_color: color(p.gamma_color, 0.0, 4.0, &format!("{base}.gammaColor"))?, gain: range(p.gain, 0.0, 4.0, format!("{base}.gain"))?, gain_color: color(p.gain_color, 0.0, 4.0, &format!("{base}.gainColor"))?, offset: range(p.offset, -1.0, 1.0, format!("{base}.offset"))?, offset_color: color(p.offset_color, 0.0, 2.0, &format!("{base}.offsetColor"))?, power: range(p.power, 0.01, 4.0, format!("{base}.power"))?, power_color: color(p.power_color, 0.0, 4.0, &format!("{base}.powerColor"))?, slope: range(p.slope, 0.0, 4.0, format!("{base}.slope"))?, slope_color: color(p.slope_color, 0.0, 4.0, &format!("{base}.slopeColor"))?, } } "exposure_contrast" => { let p: ExposureContrastParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::ExposureContrast { exposure_stops: range( p.exposure_stops, -10.0, 10.0, format!("{base}.exposureStops"), )?, contrast: range(p.contrast, 0.01, 4.0, format!("{base}.contrast"))?, pivot: range(p.pivot, 0.001, 4.0, format!("{base}.pivot"))?, factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?, } } "saturation" => { let p: SaturationParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::Saturation { saturation: range(p.saturation, 0.0, 4.0, format!("{base}.saturation"))?, factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?, } } "channel_mixer" => { let p: ChannelMixerParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::ChannelMixer { red_output: components(p.red_output, -2.0, 2.0, &format!("{base}.redOutput"))?, green_output: components( p.green_output, -2.0, 2.0, &format!("{base}.greenOutput"), )?, blue_output: components(p.blue_output, -2.0, 2.0, &format!("{base}.blueOutput"))?, factor: range(p.factor, 0.0, 1.0, format!("{base}.factor"))?, } } "bloom_extract" => { let p: BloomExtractParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::BloomExtract { threshold: range(p.threshold, 0.0, 64.0, format!("{base}.threshold"))?, knee: range(p.knee, 0.0, 1.0, format!("{base}.knee"))?, } } "bloom_blur" => { let p: BloomBlurParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; let x = range(p.direction[0], -1.0, 1.0, format!("{base}.direction[0]"))?; let y = range(p.direction[1], -1.0, 1.0, format!("{base}.direction[1]"))?; if (x.abs() + y.abs() - 1.0).abs() > 0.0001 { return Err(error( "GRAPH_PARAMETERS_INVALID", "direction must be a unit axis", format!("{base}.direction"), )); } NormalizedParameters::BloomBlur { direction: [x, y], radius: range(p.radius, 1.0, 16.0, format!("{base}.radius"))?, } } "bloom_composite" => { let p: BloomCompositeParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::BloomComposite { intensity: range(p.intensity, 0.0, 16.0, format!("{base}.intensity"))?, } } "luminance_edge" => { let p: LuminanceEdgeParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; NormalizedParameters::LuminanceEdge { strength: range(p.strength, 0.0, 16.0, format!("{base}.strength"))?, } } "frame_out" => { let p: FrameOutParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; let exposure_stops = range( p.exposure_stops, -10.0, 10.0, format!("{base}.exposureStops"), )?; let background_color = components( p.background_color, 0.0, 1.0, &format!("{base}.backgroundColor"), )?; NormalizedParameters::FrameOut { surface_format: p.surface_format, dynamic_range: if p.hdr_enabled { FrameDynamicRange::Hdr { tone_mapper: p.tone_mapper, exposure_stops, } } else { FrameDynamicRange::Sdr }, output_transfer: p.output_transfer, scale_mode: p.scale_mode, filter: p.filter, background_color, } } "texture" => { let p: TextureParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; if matches!( p.residency, TextureResidency::History | TextureResidency::Readback ) { return Err(error( "GRAPH_UNSUPPORTED_FEATURE", "history and readback textures are unsupported", format!("{base}.residency"), )); } 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")); } 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, descriptor: normalize_texture(p.texture, &base)?, } } key if contract_for(key, pipelines).is_some_and(|contract| contract.is_raster_draw()) => { let p: RasterParameters = serde_json::from_value(node.parameters.clone()).map_err(invalid)?; if !p.clear_depth.is_finite() || !(0.0..=1.0).contains(&p.clear_depth) { return Err(error( "GRAPH_PARAMETERS_INVALID", "clearDepth must be finite and in [0,1]", format!("{base}.clearDepth"), )); } if p.clear_color.iter().any(|x| !x.is_finite()) { return Err(error( "GRAPH_PARAMETERS_INVALID", "clearColor must be finite", format!("{base}.clearColor"), )); } NormalizedParameters::Raster { depth_compare: p.depth_compare, depth_write_enabled: p.depth_write_enabled, clear_depth: p.clear_depth, clear_color: p.clear_color, predicate_default: p.predicate_default, } } key if contract_for(key, pipelines) .is_some_and(|contract| contract.execution == ExecutionClass::Expression) => { let contract = contract_for(key, pipelines).unwrap(); let object = node.parameters.as_object().ok_or_else(|| { error( "GRAPH_PARAMETERS_INVALID", "parameters must be an object", base.clone(), ) })?; let default_inputs: Vec<_> = contract .inputs .iter() .filter(|input| input.cardinality.max == 1) .collect(); if object.len() != default_inputs.len() { return Err(error( "GRAPH_PARAMETERS_INVALID", "expression defaults must exactly match inputs", base, )); } let mut defaults = Vec::with_capacity(default_inputs.len()); for input in default_inputs { let key = format!("{}Default", input.name); let value = object.get(&key).ok_or_else(|| { error( "GRAPH_PARAMETERS_INVALID", "missing expression default", format!("{base}.{key}"), ) })?; let TypeConstraint::Exact(ty) = input.accepted else { unreachable!() }; defaults.push(literal(value, ty).ok_or_else(|| { error( "GRAPH_PARAMETERS_INVALID", "invalid typed expression default", format!("{base}.{key}"), ) })?); } NormalizedParameters::ExpressionDefaults { defaults } } _ => unreachable!(), }) } fn accepts(c: TypeConstraint, ty: SemanticType) -> bool { match c { TypeConstraint::Exact(x) => x == ty, TypeConstraint::OneOf(xs) => xs.contains(&ty), } } pub fn compile(graph: Graph) -> Result { super::ast::validate_pipeline_declarations(&graph)?; if graph.nodes.len() > MAX_EXECUTIONS { return Err(error( "GRAPH_LIMIT_EXCEEDED", "node count exceeds 1024", "nodes", )); } let mut input_count = 0usize; for (i, node) in graph.nodes.iter().enumerate() { input_count = input_count.saturating_add(node.inputs.values().map(Vec::len).sum::()); if input_count > 8192 { return Err(error( "GRAPH_LIMIT_EXCEEDED", "input count exceeds 8192", format!("nodes[{i}].inputs"), )); } } if graph.schema_version != 3 { return Err(GraphError::new( "GRAPH_SCHEMA_UNSUPPORTED", "schemaVersion must be 3", )); } validate_name_length(&graph.graph_id, "graphId")?; for (i, n) in graph.nodes.iter().enumerate() { for (value, path) in [ (&n.id, format!("nodes[{i}].id")), (&n.executor.key, format!("nodes[{i}].executor.key")), ] { validate_name_length(value, path)?; } for (socket, refs) in &n.inputs { validate_name_length(socket, format!("nodes[{i}].inputs.{socket}"))?; for (index, r) in refs.iter().enumerate() { validate_name_length(&r.node, format!("nodes[{i}].inputs.{socket}[{index}].node"))?; validate_name_length( &r.socket, format!("nodes[{i}].inputs.{socket}[{index}].socket"), )?; } } } validate_name_grammar(&graph.graph_id, "graphId")?; let mut ids = HashMap::new(); for (i, n) in graph.nodes.iter().enumerate() { for (value, path) in [ (&n.id, format!("nodes[{i}].id")), (&n.executor.key, format!("nodes[{i}].executor.key")), ] { validate_name_grammar(value, path)?; } if ids.insert(n.id.as_str(), i).is_some() { return Err(error( "GRAPH_DUPLICATE_ID", "duplicate node id", format!("nodes[{i}].id"), )); } for (socket, refs) in &n.inputs { validate_name_grammar(socket, format!("nodes[{i}].inputs.{socket}"))?; for (index, r) in refs.iter().enumerate() { validate_name_grammar( &r.node, format!("nodes[{i}].inputs.{socket}[{index}].node"), )?; validate_name_grammar( &r.socket, format!("nodes[{i}].inputs.{socket}[{index}].socket"), )?; } } } for (i, n) in graph.nodes.iter().enumerate() { for (s, refs) in &n.inputs { for (index, r) in refs.iter().enumerate() { if !ids.contains_key(r.node.as_str()) { return Err(error( "GRAPH_UNKNOWN_NODE", "unknown input node", format!("nodes[{i}].inputs.{s}[{index}].node"), )); } } } } let contracts: Vec<_> = graph .nodes .iter() .enumerate() .map(|(i, n)| { contract_for(&n.executor.key, &graph.pipelines).ok_or_else(|| { error( "GRAPH_UNKNOWN_EXECUTOR", "unknown executor", format!("nodes[{i}].executor.key"), ) }) }) .collect::>()?; for (i, n) in graph.nodes.iter().enumerate() { if n.executor.version != contracts[i].version { return Err(error( "GRAPH_EXECUTOR_VERSION_UNSUPPORTED", "unsupported executor version", format!("nodes[{i}].executor.version"), )); } } let params: Vec<_> = graph .nodes .iter() .enumerate() .map(|(i, n)| decode(n, i, &graph.pipelines)) .collect::>()?; 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 && n.executor.key != "frame_out" { return Err(error( "GRAPH_NODE_STATE_INVALID", "muted nodes are unsupported", format!("nodes[{i}].state"), )); } } // Socket validation is intentionally global and phased. In particular, no // cardinality or semantic error may hide a later structural socket error. for (i, n) in graph.nodes.iter().enumerate() { for name in n.inputs.keys() { if !contracts[i].inputs.iter().any(|s| s.name == name) { return Err(error( "GRAPH_UNKNOWN_SOCKET", "unknown input socket", format!("nodes[{i}].inputs.{name}"), )); } } } for (i, n) in graph.nodes.iter().enumerate() { for (name, refs) in &n.inputs { for (index, r) in refs.iter().enumerate() { let pn = ids[r.node.as_str()]; if !contracts[pn].outputs.iter().any(|out| out.name == r.socket) { return Err(error( "GRAPH_UNKNOWN_SOCKET", "unknown output socket", format!("nodes[{i}].inputs.{name}[{index}].socket"), )); } } } } for (i, n) in graph.nodes.iter().enumerate() { for input in contracts[i].inputs { let sources = n.inputs.get(input.name); if sources.is_some_and(Vec::is_empty) { return Err(error( "GRAPH_SOCKET_CARDINALITY", "present input bindings must not be empty", format!("nodes[{i}].inputs.{}", input.name), )); } let count = sources.map_or(0, Vec::len); if count < input.cardinality.min as usize || count > input.cardinality.max as usize { return Err(error( "GRAPH_SOCKET_CARDINALITY", "input binding count is outside the accepted range", format!("nodes[{i}].inputs.{}", input.name), )); } } } let mut bound: Vec> = vec![BTreeMap::new(); graph.nodes.len()]; for (i, n) in graph.nodes.iter().enumerate() { for input in contracts[i].inputs { let Some(refs) = n.inputs.get(input.name) else { continue; }; for (source_index, r) in refs.iter().enumerate() { let pn = ids[r.node.as_str()]; let (ordinal, out) = contracts[pn] .outputs .iter() .enumerate() .find(|(_, o)| o.name == r.socket) .expect("producer sockets were globally validated"); if !accepts(input.accepted, out.semantic_type) { return Err(error( "GRAPH_SOCKET_TYPE_MISMATCH", "socket type mismatch", format!("nodes[{i}].inputs.{}[{source_index}]", input.name), )); } if source_index == 0 { bound[i].insert( input.name, BoundInput { producer: OutputKey(pn, ordinal as u16), }, ); } } } } let mut edges = Vec::new(); for i in 0..graph.nodes.len() { for (input_ordinal, input) in contracts[i].inputs.iter().enumerate() { for (source_index, b) in graph.nodes[i] .inputs .get(input.name) .into_iter() .flatten() .enumerate() .map(|(source_index, r)| { let pn = ids[r.node.as_str()]; let ordinal = contracts[pn] .outputs .iter() .position(|o| o.name == r.socket) .unwrap(); ( source_index, BoundInput { producer: OutputKey(pn, ordinal as u16), }, ) }) { edges.push(DependencyEdge { from_node: b.producer.0, from_socket: contracts[b.producer.0].outputs[b.producer.1 as usize] .name .into(), producer_output_ordinal: b.producer.1, to_node: i, to_socket: input.name.into(), consumer_input_ordinal: input_ordinal as u16, resource: graph.nodes[i].inputs[input.name][source_index].clone(), }); } } } edges.sort_by_key(|e| { ( e.to_node, e.consumer_input_ordinal, e.from_node, 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 readers that must finish before an attachment version is overwritten. let mut ordinary_consumers = HashMap::>::new(); for edge in &edges { let key = OutputKey(edge.from_node, edge.producer_output_ordinal); if !is_normalizable_target(edge) && is_exact_reader(edge) && provisional_live.contains(&edge.to_node) { ordinary_consumers .entry(key) .or_default() .push(edge.to_node); } } let authored_depends_on = |consumer: usize, dependency: usize| { let mut seen = vec![false; graph.nodes.len()]; let mut pending = vec![consumer]; while let Some(node) = pending.pop() { if node == dependency { return true; } if !seen[node] { seen[node] = true; pending.extend(authored_deps[node].iter().copied()); } } false }; // Attachment normalization may replace an authored raster-to-raster edge, // but it must not erase the ordering constraint expressed by that edge. // A constraint opposite to authored cohort order is therefore reported by // the ordinary cycle detector below. let mut ordering_edges: Vec<_> = edges .iter() .filter(|edge| is_normalizable_target(edge)) .map(|edge| (edge.from_node, edge.to_node)) .collect(); // 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 { 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 { "color" } else { "depth" }; 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::::new(); for ordinal in 0..=1u16 { let mut colors = HashMap::new(); let mut roots = HashMap::new(); let mut cohorts = BTreeMap::>::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_unstable(); // 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 output = OutputKey(members[position], ordinal); let at_cut = members.get(position + 1).is_some_and(|&next_writer| { ordinary_consumers.get(&output).is_some_and(|readers| { readers.iter().any(|&reader| { contracts[reader].fullscreen_policy.is_some() && !authored_depends_on(reader, next_writer) }) }) }); 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 { "color" } else { "depth" }; 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 == "color" || edge.to_socket == "depth")) }); for &node in &raster_nodes { for (socket, ordinal) in [("color", 0u16), ("depth", 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); } for node_deps in &mut deps { node_deps.sort(); node_deps.dedup(); } let mut live = HashSet::new(); let mut stack: Vec<_> = contracts .iter() .enumerate() .filter(|(i, c)| c.inherently_observable && graph.nodes[*i].state == NodeState::Enabled) .map(|(i, _)| i) .collect(); while let Some(i) = stack.pop() { if live.insert(i) { stack.extend(deps[i].iter().copied()); } } // 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() { if out.semantic_type.is_virtual() { continue; } 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(key, id); resource_meta.push((i, o as u16, *out)); } } } let all_outputs: usize = contracts .iter() .flat_map(|contract| contract.outputs) .filter(|output| !output.semantic_type.is_virtual()) .count(); let authored_materialized_output_count = output_ids.len(); // Establish families and transitions without relying on a schedule. let mut families = Vec::new(); let mut source_family = HashMap::new(); for i in 0..graph.nodes.len() { if !live.contains(&i) { continue; } let source = output_ids.get(&OutputKey(i, 0)).copied(); match ¶ms[i] { NormalizedParameters::Texture { residency, descriptor, } => { let id = families.len() as u32; let r = source.unwrap(); source_family.insert(TransitionTargetKey::Authored(OutputKey(i, 0)), id); families.push(TextureFamily { id, key: TextureFamilyKey { source_node: i as u32, source_socket: 0, }, source: TextureFamilySource::AuthoredTexture { resource: r, residency: *residency, descriptor: descriptor.clone(), }, lifetime: Lifetime { first_use: 0, last_use: 0, }, versions: vec![], usage: vec![], allocation: None, aliasable: false, }); } _ => {} } } // Reserve compiler-owned roots without exposing incomplete public plan entries. // IDs remain stable while effective families and descriptors are resolved. let full_extent = NormalizedTextureExtent::SurfaceRelative { width: Ratio { numerator: 1, denominator: 1, }, height: Ratio { numerator: 1, denominator: 1, }, depth_or_array_layers: 1, }; let authored_family_count = families.len(); let mut default_roots: Vec = Vec::new(); let mut default_targets = HashMap::new(); for i in 0..graph.nodes.len() { if !live.contains(&i) || !contracts[i].is_raster_draw() { continue; } for (socket, role, format, opposite) in [ ( "color", CompilerTextureRole::ColorTarget, TextureFormat::Rgba16Float, "depth", ), ( "depth", CompilerTextureRole::DepthTarget, TextureFormat::Depth32Float, "color", ), ] .into_iter() { if bound[i].contains_key(socket) { continue; } 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, }; let resource = (authored_materialized_output_count + default_roots.len()) as u32; let family = (authored_family_count + default_roots.len()) as u32; default_targets.insert((i, socket), key); source_family.insert(key, family); let _ = opposite; default_roots.push(DefaultDraft { key, resource, family, owner_node: i, input_ordinal, socket, role, format, descriptor: DraftDescriptor::Deferred, }); } } let mut transitions: Vec = Vec::new(); let mut transitions_by_target: BTreeMap> = BTreeMap::new(); for i in 0..graph.nodes.len() { if !live.contains(&i) { continue; } let transition_sockets: &[(&str, u16)] = match contracts[i] { ref contract if contract.is_raster_draw() => &[("color", 0), ("depth", 1)], _ if contracts[i].fullscreen_policy.is_some() => &[("colorTarget", 0)], _ => continue, }; for &(input_socket, output_ordinal) in transition_sockets { let transition = TextureTransition { writer_node: i, input_socket, target: bound[i] .get(input_socket) .map(|b| TransitionTargetKey::Authored(b.producer)) .unwrap_or_else(|| default_targets[&(i, input_socket)]), output: OutputKey(i, output_ordinal), }; let index = transitions.len(); transitions.push(transition); transitions_by_target .entry(transition.target) .or_default() .push(index); } } fn resolve_transition( output: OutputKey, transitions: &[TextureTransition], transition_for_output: &HashMap, source_family: &HashMap, colors: &mut HashMap, resolved: &mut HashMap, ) -> ResolvedTransition { if let Some(&value) = resolved.get(&output) { return value; } if colors.get(&output) == Some(&1) { return ResolvedTransition::Cyclic; } colors.insert(output, 1); let transition = transitions[transition_for_output[&output]]; let value = if let Some(&family) = source_family.get(&transition.target) { ResolvedTransition::Resolved { family, version: 0, target: transition.target, } } else if let TransitionTargetKey::Authored(target_output) = transition.target { if !transition_for_output.contains_key(&target_output) { ResolvedTransition::Cyclic } else { match resolve_transition( target_output, transitions, transition_for_output, source_family, colors, resolved, ) { ResolvedTransition::Resolved { family, version, .. } => ResolvedTransition::Resolved { family, version: version + 1, target: transition.target, }, ResolvedTransition::Cyclic => ResolvedTransition::Cyclic, } } } else { ResolvedTransition::Cyclic }; colors.insert(output, 2); resolved.insert(output, value); value } let transition_for_output: HashMap<_, _> = transitions .iter() .enumerate() .map(|(index, transition)| (transition.output, index)) .collect(); let mut resolved = HashMap::new(); let mut colors = HashMap::new(); for transition in &transitions { resolve_transition( transition.output, &transitions, &transition_for_output, &source_family, &mut colors, &mut resolved, ); } let mut version_of: HashMap = HashMap::new(); for transition in &transitions { if let ResolvedTransition::Resolved { family, version, target, } = resolved[&transition.output] { let target_id = match target { TransitionTargetKey::Authored(key) => output_ids[&key], TransitionTargetKey::CompilerDefaultInput { .. } => { default_roots .iter() .find(|root| root.key == target) .unwrap() .resource } }; version_of.insert(transition.output, (family, version, target_id)); } } let mut outgoing_edges = vec![Vec::new(); graph.nodes.len()]; for (index, edge) in edges.iter().enumerate() { if live.contains(&edge.from_node) && live.contains(&edge.to_node) { 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]; ( edge.to_node, edge.producer_output_ordinal, edge.consumer_input_ordinal, ) }); } // Same-pass hazards are global and precede every duplicate-writer diagnostic. for i in 0..graph.nodes.len() { if !live.contains(&i) { continue; } if contracts[i].is_raster_draw() { if bound[i].get("color").map(|b| b.producer) == bound[i].get("depth").map(|b| b.producer) && bound[i].contains_key("color") || matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df) { return Err(error( "GRAPH_SAME_PASS_HAZARD", "color and depth use one texture family", format!("nodes[{i}].inputs"), )); } } else if contracts[i].fullscreen_policy.is_some() { 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 { return Err(error( "GRAPH_SAME_PASS_HAZARD", "copy source and target use one texture family", format!("nodes[{i}].inputs"), )); } } } let mut first_writer = BTreeMap::new(); for transition in &transitions { if first_writer .insert(transition.target, transition.writer_node) .is_some_and(|writer| writer != transition.writer_node) { return Err(error( "GRAPH_DUPLICATE_WRITER", "texture version has multiple writers", format!( "nodes[{}].inputs.{}", transition.writer_node, transition.input_socket ), )); } } // Infer draft descriptors without recursion. Unknown and invalid dependencies // remain deferred so descriptor diagnostics never steal cycle diagnostics. for _ in 0..default_roots.len() { let mut changed = false; for index in 0..default_roots.len() { if matches!(default_roots[index].descriptor, DraftDescriptor::Known(_)) { continue; } let owner = default_roots[index].owner_node; let opposite_output = if default_roots[index].role == CompilerTextureRole::ColorTarget { OutputKey(owner, 1) } else { OutputKey(owner, 0) }; let Some(&(opposite_family, _, _)) = version_of.get(&opposite_output) else { continue; }; let opposite_descriptor = known_family_descriptor(opposite_family, &families, &default_roots); let extent = if opposite_family as usize >= authored_family_count && default_roots .get(opposite_family as usize - authored_family_count) .is_some_and(|draft| draft.owner_node == owner) { Some(full_extent.clone()) } else { opposite_descriptor.map(|descriptor| descriptor.extent.clone()) }; if let Some(extent) = extent { default_roots[index].descriptor = DraftDescriptor::Known(NormalizedTextureDescriptor { dimension: TextureDimension::D2, format: default_roots[index].format, extent, mip_level_count: 1, sample_count: opposite_descriptor .map_or(1, |descriptor| descriptor.sample_count), view_formats: vec![], }); changed = true; } } if !changed { break; } } // Classify sampled edges before descriptor and stale-version validation. A // demanded resolve is keyed by the exact symbolic output, not its family. let mut resolve_demands = BTreeSet::::new(); for edge in &edges { if !live.contains(&edge.to_node) || contracts[edge.to_node].inputs[edge.consumer_input_ordinal as usize].role != InputRole::SampledTexture { continue; } let key = OutputKey(edge.from_node, edge.producer_output_ordinal); let Some(&(family, _, _)) = version_of.get(&key) else { if matches!(resolved.get(&key), Some(ResolvedTransition::Cyclic)) { continue; } if source_family .get(&TransitionTargetKey::Authored(key)) .and_then(|&family| known_family_descriptor(family, &families, &default_roots)) .is_some_and(|descriptor| descriptor.sample_count == 4) { return Err(error( "GRAPH_ILLEGAL_ACCESS", "unproduced multisampled texture cannot be sampled", format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket), )); } continue; }; let Some(descriptor) = known_family_descriptor(family, &families, &default_roots) else { continue; }; if descriptor.sample_count == 1 { continue; } if descriptor.format == TextureFormat::Depth32Float { return Err(error( "GRAPH_ILLEGAL_ACCESS", "multisampled depth texture cannot be sampled", format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket), )); } 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", format!("nodes[{}].inputs.{}", edge.to_node, edge.to_socket), )); } resolve_demands.insert(key); } // Every ordinary texture reader must execute before a successor overwrites // its allocation. Resolve demands read the attachment during its producer. let mut reachability = HashMap::new(); for (i, contract) in contracts.iter().enumerate() { if !live.contains(&i) { continue; } for input in contract .inputs .iter() .filter(|input| input.role == InputRole::SampledTexture) { let key = bound[i][input.name].producer; if resolve_demands.contains(&key) || !version_of.contains_key(&key) { continue; } let Some(next_indices) = transitions_by_target.get(&TransitionTargetKey::Authored(key)) else { continue; }; let [next_index] = next_indices.as_slice() else { continue; }; let next = transitions[*next_index]; if i != next.writer_node && !reaches( i, next.writer_node, &outgoing_edges, &ordering_outgoing, &edges, &live, &mut reachability, ) { return Err(error( "GRAPH_RESOURCE_VERSION_INVALID", "older texture version may be read after its successor", format!("nodes[{i}].inputs.{}", input.name), )); } } } // Validate every independently resolved attachment before graph cycle reporting. for i in 0..graph.nodes.len() { if !live.contains(&i) || !contracts[i].is_raster_draw() { continue; } let cd = version_of .get(&OutputKey(i, 0)) .and_then(|&(family, _, _)| known_family_descriptor(family, &families, &default_roots)); let dd = version_of .get(&OutputKey(i, 1)) .and_then(|&(family, _, _)| known_family_descriptor(family, &families, &default_roots)); let ok_depth = dd.is_none_or(|dd| { dd.dimension == TextureDimension::D2 && dd.format == TextureFormat::Depth32Float && matches!(dd.sample_count, 1 | 4) && dd.mip_level_count == 1 && dd.view_formats.is_empty() && extent_layers(&dd.extent) == 1 }); let ok_color = cd.is_none_or(|cd| { cd.dimension == TextureDimension::D2 && cd.format != TextureFormat::Depth32Float && matches!(cd.sample_count, 1 | 4) && cd.mip_level_count == 1 && cd.view_formats.is_empty() && extent_layers(&cd.extent) == 1 }); if !ok_color { return Err(error( "GRAPH_ILLEGAL_ACCESS", "color attachment is invalid", format!("nodes[{i}].inputs.color"), )); } if !ok_depth { return Err(error( "GRAPH_ILLEGAL_ACCESS", "depth attachment is invalid", format!("nodes[{i}].inputs.depth"), )); } if let (Some(cd), Some(dd)) = (cd, dd) { if cd.dimension != dd.dimension || cd.extent != dd.extent || cd.sample_count != dd.sample_count { return Err(error( "GRAPH_ILLEGAL_ACCESS", "attachments are incompatible", format!("nodes[{i}].inputs"), )); } } } for i in 0..graph.nodes.len() { if !live.contains(&i) || contracts[i].fullscreen_policy.is_none() { continue; } let source = sampled_descriptor( bound[i]["source"].producer, &version_of, &resolved, &families, &default_roots, )?; let source_descriptor = match source { SampledDescriptor::Known(descriptor) => Some(descriptor), SampledDescriptor::Deferred | SampledDescriptor::Unproduced => None, }; let target_family_id = version_of .get(&OutputKey(i, 0)) .map(|&(family, _, _)| family) .or_else(|| { source_family .get(&TransitionTargetKey::Authored( bound[i]["colorTarget"].producer, )) .copied() }); let target_descriptor = target_family_id .and_then(|family| known_family_descriptor(family, &families, &default_roots)); let bloom = if contracts[i].fullscreen_policy == Some(FullscreenPolicy::BloomComposite) { Some(sampled_descriptor( bound[i]["bloom"].producer, &version_of, &resolved, &families, &default_roots, )?) } else { None }; let bloom_descriptor = match &bloom { Some(SampledDescriptor::Known(descriptor)) => Some(*descriptor), _ => None, }; let source_ok = source_descriptor.is_none_or(|descriptor| { descriptor.format == TextureFormat::Rgba16Float && (is_single_view_d2(descriptor) || resolve_demands.contains(&bound[i]["source"].producer)) }); let target_ok = target_descriptor.is_none_or(|descriptor| { is_single_view_d2(descriptor) && match contracts[i].fullscreen_policy { Some(FullscreenPolicy::Copy) => { descriptor.format != TextureFormat::Depth32Float } Some(FullscreenPolicy::BloomExtract) | Some(FullscreenPolicy::HdrSameExtent) | Some(FullscreenPolicy::BloomComposite) => { descriptor.format == TextureFormat::Rgba16Float } _ => false, } }); let bloom_ok = bloom_descriptor.is_none_or(|descriptor| { descriptor.format == TextureFormat::Rgba16Float && (is_single_view_d2(descriptor) || resolve_demands.contains(&bound[i]["bloom"].producer)) }); let extent_ok = match contracts[i].fullscreen_policy { Some(FullscreenPolicy::Copy) | Some(FullscreenPolicy::HdrSameExtent) | Some(FullscreenPolicy::BloomComposite) => { !matches!((source_descriptor, target_descriptor), (Some(source), Some(target)) if source.extent != target.extent) } Some(FullscreenPolicy::BloomExtract) => true, _ => false, }; if !source_ok || !target_ok || !bloom_ok || !extent_ok { return Err(error( "GRAPH_ILLEGAL_ACCESS", "fullscreen textures are incompatible", format!("nodes[{i}].inputs"), )); } if matches!(source, SampledDescriptor::Unproduced) { return Err(error( "GRAPH_UNINITIALIZED_RESOURCE", "copy source is not produced", format!("nodes[{i}].inputs.source"), )); } if matches!(bloom, Some(SampledDescriptor::Unproduced)) { return Err(error( "GRAPH_UNINITIALIZED_RESOURCE", "bloom source is not produced", format!("nodes[{i}].inputs.bloom"), )); } } // Frame output must consume an initialized, produced, filterable color texture. for (i, contract) in contracts.iter().enumerate() { if !live.contains(&i) || contract.key != "frame_out" { continue; } 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", "frame output source is not produced", format!("nodes[{i}].inputs.color"), )); } continue; }; let Some(descriptor) = known_family_descriptor(family, &families, &default_roots) else { continue; }; let NormalizedParameters::FrameOut { dynamic_range, .. } = ¶ms[i] else { unreachable!() }; let mut effective = descriptor.clone(); if resolve_demands.contains(&key) { effective.sample_count = 1; } if !frame_out_source_compatible(&effective, dynamic_range) { let message = match dynamic_range { FrameDynamicRange::Hdr { .. } => "HDR frame output requires rgba16_float", FrameDynamicRange::Sdr => { "SDR frame output requires a linear filterable color texture" } }; return Err(error( "GRAPH_ILLEGAL_ACCESS", message, format!("nodes[{i}].inputs.color"), )); } } // Stable Kahn scheduling is deliberately after resource and access validation. let mut indegree = vec![0; graph.nodes.len()]; for &node in &live { indegree[node] = edges .iter() .filter(|edge| edge.to_node == node && live.contains(&edge.from_node)) .count() + ordering_edges .iter() .filter(|(from, to)| *to == node && live.contains(from)) .count(); } let mut queue = BinaryHeap::new(); for &node in &live { if indegree[node] == 0 { queue.push(Reverse(node)); } } let mut order = Vec::new(); while let Some(Reverse(node)) = queue.pop() { order.push(node); for &edge_index in &outgoing_edges[node] { let consumer = edges[edge_index].to_node; indegree[consumer] -= 1; if indegree[consumer] == 0 { 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()) .map(|node| live.contains(&node) && indegree[node] != 0) .collect(); fn cycle_dfs( node: usize, outgoing_edges: &[Vec], ordering_outgoing: &[Vec], edges: &[DependencyEdge], residual: &[bool], colors: &mut [u8], node_stack: &mut Vec, edge_stack: &mut Vec, ) -> Option> { colors[node] = 1; node_stack.push(node); for &edge_index in &outgoing_edges[node] { let to = edges[edge_index].to_node; if !residual[to] { continue; } if colors[to] == 0 { edge_stack.push(edge_index); if let Some(cycle) = cycle_dfs( to, outgoing_edges, ordering_outgoing, edges, residual, colors, node_stack, edge_stack, ) { return Some(cycle); } edge_stack.pop(); } else if colors[to] == 1 { let position = node_stack .iter() .position(|&stacked| stacked == to) .unwrap(); let mut cycle = edge_stack[position..].to_vec(); cycle.push(edge_index); 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 } let mut colors = vec![0; graph.nodes.len()]; let mut cycle = None; for node in 0..graph.nodes.len() { if residual[node] && colors[node] == 0 { cycle = cycle_dfs( node, &outgoing_edges, &ordering_outgoing, &edges, &residual, &mut colors, &mut Vec::new(), &mut Vec::new(), ); if cycle.is_some() { break; } } } let mut graph_error = GraphError::new("GRAPH_CYCLE", "live graph contains a cycle"); let payload: Vec<_> = cycle .unwrap_or_default() .into_iter() .map(|index| { let edge = &edges[index]; serde_json::json!({ "fromNode": graph.nodes[edge.from_node].id, "fromSocket": edge.from_socket, "toNode": graph.nodes[edge.to_node].id, "toSocket": edge.to_socket, "resource": edge.resource, }) }) .collect(); graph_error.details = serde_json::json!({"message":graph_error.message,"kind":"cycle","edges":payload}); return Err(graph_error); } if transitions .iter() .any(|transition| matches!(resolved[&transition.output], ResolvedTransition::Cyclic)) || default_roots .iter() .any(|draft| matches!(draft.descriptor, DraftDescriptor::Deferred)) { return Err(error( "GRAPH_RESOURCE_VERSION_INVALID", "texture predecessor is unresolved in an acyclic graph", "resources", )); } for draft in &default_roots { let DraftDescriptor::Known(descriptor) = &draft.descriptor else { unreachable!("deferred drafts rejected above") }; families.push(TextureFamily { id: draft.family, key: TextureFamilyKey { source_node: draft.owner_node as u32, source_socket: draft.input_ordinal, }, source: TextureFamilySource::CompilerDefaultInput { resource: draft.resource, owner_node_index: draft.owner_node as u32, input_ordinal: draft.input_ordinal, role: draft.role, descriptor: descriptor.clone(), }, lifetime: Lifetime { first_use: 0, last_use: 0, }, versions: vec![], usage: vec![], allocation: None, aliasable: false, }); } for transition in &transitions { if let Some(&(family, version, target)) = version_of.get(&transition.output) { families[family as usize].versions.push(TextureVersion { version, resource: output_ids[&transition.output], target, initialized: true, stored: true, lifetime: Lifetime { first_use: 0, last_use: 0, }, }); } } for family in &mut families { family.versions.sort_by_key(|version| version.version); if family .versions .iter() .enumerate() .any(|(index, version)| version.version != index as u32) { return Err(error( "GRAPH_RESOURCE_VERSION_INVALID", "texture versions must form a dense linear chain", "resources", )); } } let mut resources = Vec::new(); for (i, o, out) in resource_meta { let key = OutputKey(i, o); let id = output_ids[&key]; let plan = match out.semantic_type { SemanticType::Texture if matches!(params[i], NormalizedParameters::Texture { .. }) => { if let NormalizedParameters::Texture { residency, descriptor, } = ¶ms[i] { ResourcePlan::TextureSource { family: source_family[&TransitionTargetKey::Authored(key)], residency: *residency, descriptor: descriptor.clone(), } } else { unreachable!() } } SemanticType::Texture => { let (f, v, t) = version_of[&key]; ResourcePlan::Texture { family: f, version: v, target: t, initialized: true, stored: true, allocation: None, } } SemanticType::MeshData => ResourcePlan::MeshData, SemanticType::Bool | SemanticType::F32 | SemanticType::U32 | SemanticType::Vec2 | SemanticType::Vec3 | SemanticType::Vec4 | SemanticType::Mat2 | SemanticType::Mat3 | SemanticType::Mat4 | SemanticType::U32x16 | SemanticType::LocalAabb => { unreachable!("pure expression outputs are never materialized") } }; resources.push(CompiledResource { original_node_index: i as u32, origin: ResourceOrigin::AuthoredOutput { node: graph.nodes[i].id.clone(), socket: out.name.into(), output_ordinal: o, }, semantic_type: out.semantic_type, producer_execution: None, lifetime: None, plan, }); let _ = id; } for draft in &default_roots { let DraftDescriptor::Known(descriptor) = &draft.descriptor else { unreachable!("deferred drafts rejected above") }; resources.push(CompiledResource { original_node_index: draft.owner_node as u32, origin: ResourceOrigin::CompilerDefaultInput { owner_node_index: draft.owner_node as u32, input_ordinal: draft.input_ordinal, socket: draft.socket.into(), role: draft.role, }, semantic_type: SemanticType::Texture, producer_execution: None, lifetime: None, plan: ResourcePlan::TextureSource { family: draft.family, residency: TextureResidency::Transient, descriptor: descriptor.clone(), }, }); } // A multisampled pipeline color remains the authored attachment version. Sampling // that exact version instead addresses one compiler-owned fixed-function resolve. let mut resolve_resources = BTreeMap::new(); for producer in resolve_demands { let (family, _, _) = version_of[&producer]; let descriptor = family_descriptor(&families[family as usize]); let source_resource = output_ids[&producer]; let root = resources.len() as u32; let resource = root + 1; let family_id = families.len() as u32; let mut resolved_descriptor = descriptor.clone(); resolved_descriptor.sample_count = 1; resolve_resources.insert(producer, resource); resources.push(CompiledResource { original_node_index: producer.0 as u32, origin: ResourceOrigin::CompilerColorResolve { producer_node_index: producer.0 as u32, output_ordinal: producer.1, source_resource, }, semantic_type: SemanticType::Texture, producer_execution: None, lifetime: None, plan: ResourcePlan::TextureSource { family: family_id, residency: TextureResidency::Transient, descriptor: resolved_descriptor.clone(), }, }); resources.push(CompiledResource { original_node_index: producer.0 as u32, origin: ResourceOrigin::CompilerColorResolve { producer_node_index: producer.0 as u32, output_ordinal: producer.1, source_resource, }, semantic_type: SemanticType::Texture, producer_execution: None, lifetime: None, plan: ResourcePlan::Texture { family: family_id, version: 0, target: root, initialized: true, stored: true, allocation: None, }, }); families.push(TextureFamily { id: family_id, key: TextureFamilyKey { source_node: producer.0 as u32, source_socket: producer.1, }, source: TextureFamilySource::CompilerColorResolve { resource: root, descriptor: resolved_descriptor, producer_node_index: producer.0 as u32, output_ordinal: producer.1, source_resource, }, lifetime: Lifetime { first_use: 0, last_use: 0, }, versions: vec![TextureVersion { version: 0, resource, target: root, initialized: true, stored: true, lifetime: Lifetime { first_use: 0, last_use: 0, }, }], usage: vec![], allocation: None, aliasable: false, }); } let mut executions = Vec::new(); for &i in &order { if matches!( contracts[i].execution, ExecutionClass::Source | ExecutionClass::Expression ) { continue; } let input_resource = |s: &str| { let key = bound[i][s].producer; let resource = output_ids[&key]; if contracts[i] .inputs .iter() .any(|input| input.name == s && input.role == InputRole::SampledTexture) { resolve_resources.get(&key).copied().unwrap_or(resource) } else { resource } }; let mut inputs = Vec::new(); for (input_ordinal, s) in contracts[i].inputs.iter().enumerate() { if s.role != InputRole::Expression { let resource = if let Some(b) = bound[i].get(s.name) { let resource = output_ids[&b.producer]; if s.role == InputRole::SampledTexture { resolve_resources .get(&b.producer) .copied() .unwrap_or(resource) } else { resource } } else if s.default_policy == InputDefaultPolicy::CompilerTexture { let key = TransitionTargetKey::CompilerDefaultInput { owner_node: i, input_ordinal: input_ordinal as u16, }; default_roots .iter() .find(|root| root.key == key) .expect("default policy materialized a root") .resource } else { continue; }; inputs.push(CompiledSocketInput { socket: s.name.into(), resource, }); } } let outputs: Vec<_> = contracts[i] .outputs .iter() .enumerate() .filter(|(_, s)| !s.semantic_type.is_virtual()) .map(|(o, s)| CompiledSocketOutput { socket: s.name.into(), resource: output_ids[&OutputKey(i, o as u16)], }) .collect(); let mut accesses = Vec::new(); let kind = match contracts[i].key { _ 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::Raster { clear_color, .. } => clear_color, _ => unreachable!(), }; let clear_depth = match ¶ms[i] { NormalizedParameters::Raster { clear_depth, .. } => *clear_depth, _ => unreachable!(), }; let first_color = version_of[&OutputKey(i, 0)].1 == 0; let first_depth = version_of[&OutputKey(i, 1)].1 == 0; let resolve_target = resolve_resources.get(&OutputKey(i, 0)).copied(); let source_store = if resolve_target.is_some() && !edges.iter().any(|edge| { edge.from_node == i && edge.producer_output_ordinal == 0 && matches!( contracts[edge.to_node].inputs [edge.consumer_input_ordinal as usize] .role, InputRole::ColorTarget { .. } ) }) { StoreOp::Discard } else { StoreOp::Store }; let stored = source_store == StoreOp::Store; if let ResourcePlan::Texture { family, version, stored: resource_stored, .. } = &mut resources[color as usize].plan { *resource_stored = stored; if let Some(texture_version) = families .get_mut(*family as usize) .and_then(|family| family.versions.get_mut(*version as usize)) { texture_version.stored = stored; } } let cl = if first_color { NormalizedColorLoad::Clear { value: clear } } else { NormalizedColorLoad::Load }; let dl = if first_depth { NormalizedDepthLoad::Clear { value: clear_depth } } else { NormalizedDepthLoad::Load }; for s in ["mesh"] { accesses.push(CompiledAccess { socket: s.into(), resource: input_resource(s), mode: AccessMode::SemanticRead, }); } accesses.push(CompiledAccess { socket: "color".into(), resource: color, mode: AccessMode::ColorAttachment { location: 0, load: cl, store: source_store, full_overwrite: first_color, }, }); accesses.push(CompiledAccess { socket: "depth".into(), resource: depth, mode: AccessMode::DepthAttachment { load: dl, store: StoreOp::Store, full_overwrite: first_depth, }, }); if let Some(resource) = resolve_target { accesses.push(CompiledAccess { socket: "colorResolve".into(), resource, mode: AccessMode::ColorResolve { source: color, location: 0, }, }); } ExecutionKind::RasterDraw } _ if contracts[i].fullscreen_policy.is_some() => { let color = output_ids[&OutputKey(i, 0)]; let load = NormalizedColorLoad::Clear { value: [0.0, 0.0, 0.0, 0.0], }; for input in contracts[i] .inputs .iter() .filter(|input| input.role == InputRole::SampledTexture) { accesses.push(CompiledAccess { socket: input.name.into(), resource: input_resource(input.name), mode: AccessMode::SampledTexture, }); } accesses.push(CompiledAccess { socket: "color".into(), resource: color, mode: AccessMode::ColorAttachment { location: 0, load, store: StoreOp::Store, full_overwrite: true, }, }); ExecutionKind::Fullscreen } "frame_out" => { let r = input_resource("color"); accesses.push(CompiledAccess { socket: "color".into(), resource: r, mode: AccessMode::SampledTexture, }); ExecutionKind::FrameOut { color: r } } _ => unreachable!(), }; executions.push(CompiledExecution { id: graph.nodes[i].id.clone(), original_node_index: i as u32, executor: graph.nodes[i].executor.clone(), parameters: params[i].clone(), kind, inputs, outputs, accesses, }); } // Lower virtual values into a stable, device-independent expression plan. let mut expression_plan = ExpressionPlan::default(); let mut expression_ids = HashMap::::new(); let mut expression_provenance = HashMap::>::new(); let mut cse = HashMap::::new(); let mut requires_camera = false; let mut mesh_root = None; let mut intern = |semantic_type: SemanticType, op: ExpressionOp, origin: NodeOutputRef, mesh_provenance: Option| { let key = format!("{semantic_type:?}:{op:?}:{mesh_provenance:?}"); if let Some(id) = cse.get(&key) { return *id; } let id = ExprId(expression_plan.expressions.len() as u32); expression_plan.expressions.push(Expression { semantic_type, op, origin, mesh_provenance, }); cse.insert(key, id); id }; for &i in &order { if contracts[i].execution != ExecutionClass::Expression { continue; } let defaults: &[TypedLiteral] = match ¶ms[i] { NormalizedParameters::ExpressionDefaults { defaults } => defaults.as_slice(), NormalizedParameters::FrustumCull { .. } => &[], _ => unreachable!(), }; let mut operands = Vec::new(); let mut operand_provenance = Vec::new(); for (ordinal, input) in contracts[i].inputs.iter().enumerate() { let bindings: Vec<_> = graph.nodes[i] .inputs .get(input.name) .into_iter() .flatten() .map(|r| { let producer = ids[r.node.as_str()]; let output = contracts[producer] .outputs .iter() .position(|o| o.name == r.socket) .unwrap(); BoundInput { producer: OutputKey(producer, output as u16), } }) .collect(); if bindings.is_empty() { if input.cardinality.max > 1 { continue; } let literal = defaults[ordinal].clone(); operands.push(intern( literal.semantic_type(), ExpressionOp::Literal { literal }, NodeOutputRef { node: graph.nodes[i].id.clone(), socket: input.name.into(), }, None, )); operand_provenance.push(None); } else { for binding in bindings { let key = binding.producer; let producer_type = contracts[key.0].outputs[key.1 as usize].semantic_type; let operand_mesh = if contracts[key.0].key == "mesh" { Some(output_ids[&OutputKey(key.0, 0)]) } else { expression_provenance[&key] }; let id = if producer_type.is_virtual() { if contracts[key.0].key == "mesh" { let mesh = output_ids[&OutputKey(key.0, 0)]; if mesh_root.is_some_and(|root| root != mesh) { return Err(error( "GRAPH_SOCKET_TYPE_MISMATCH", "instance traversal has multiple mesh roots", format!("nodes[{i}].inputs.{}", input.name), )); } mesh_root = Some(mesh); let op = match producer_type { SemanticType::U32x16 => ExpressionOp::InstanceType { mesh }, SemanticType::LocalAabb => ExpressionOp::LocalAabb { mesh }, _ => unreachable!(), }; intern( producer_type, op, graph.nodes[key.0] .inputs .get("") .and_then(|refs| refs.first().cloned()) .unwrap_or(NodeOutputRef { node: graph.nodes[key.0].id.clone(), socket: contracts[key.0].outputs[key.1 as usize] .name .into(), }), Some(mesh), ) } else { expression_ids[&key] } } else { continue; }; operands.push(id); operand_provenance.push(operand_mesh); } } } let mut provenances = operand_provenance.into_iter().flatten(); let provenance = provenances.next(); if provenances.any(|candidate| Some(candidate) != provenance) { return Err(error( "GRAPH_SOCKET_TYPE_MISMATCH", "expression mixes mesh provenance", format!("nodes[{i}].inputs"), )); } for (output_ordinal, output) in contracts[i].outputs.iter().enumerate() { let key = contracts[i].key; let op = match key { "not" => ExpressionOp::Not { value: operands[0] }, "and" | "or" | "xor" | "xnor" => ExpressionOp::Boolean { operation: match key { "and" => BooleanOp::And, "or" => BooleanOp::Or, "xor" => BooleanOp::Xor, _ => BooleanOp::Xnor, }, operands: operands.clone(), }, "greater_than_f32" | "less_than_f32" | "equals_f32" => ExpressionOp::CompareF32 { operation: if key.starts_with("greater") { CompareOp::GreaterThan } else if key.starts_with("less") { CompareOp::LessThan } else { CompareOp::Equals }, left: operands[0], right: operands[1], }, "greater_than_u32" | "less_than_u32" | "equals_u32" => ExpressionOp::CompareU32 { operation: if key.starts_with("greater") { CompareOp::GreaterThan } else if key.starts_with("less") { CompareOp::LessThan } else { CompareOp::Equals }, left: operands[0], right: operands[1], }, k if k.starts_with("separate_vec") => ExpressionOp::VectorProject { vector: operands[0], index: output_ordinal as u8, }, k if k.starts_with("combine_vec") => ExpressionOp::VectorConstruct { components: operands.clone(), }, k if k.starts_with("separate_mat") => ExpressionOp::MatrixColumn { matrix: operands[0], index: output_ordinal as u8, }, k if k.starts_with("combine_mat") => ExpressionOp::MatrixConstruct { columns: operands.clone(), }, "separate_u32x16" => ExpressionOp::TypeWord { value: operands[0], index: output_ordinal as u8, }, "combine_u32x16" => ExpressionOp::TypeConstruct { words: operands.clone(), }, "separate_u32_bits" => ExpressionOp::U32Bit { value: operands[0], index: output_ordinal as u8, }, "combine_u32_bits" => ExpressionOp::U32Construct { bits: operands.clone(), }, "separate_local_aabb" if output_ordinal == 0 => { ExpressionOp::AabbMin { aabb: operands[0] } } "separate_local_aabb" => ExpressionOp::AabbMax { aabb: operands[0] }, "frustum_cull" => { requires_camera = true; let mesh = output_ids[&bound[i]["mesh"].producer]; if mesh_root.is_some_and(|root| root != mesh) { return Err(error( "GRAPH_SOCKET_TYPE_MISMATCH", "instance traversal has multiple mesh roots", format!("nodes[{i}].inputs.mesh"), )); } mesh_root = Some(mesh); ExpressionOp::FrustumCulled { mesh, local_aabb: operands[0], } } _ => unreachable!(), }; let id = intern( output.semantic_type, op, NodeOutputRef { node: graph.nodes[i].id.clone(), socket: output.name.into(), }, provenance, ); expression_ids.insert(OutputKey(i, output_ordinal as u16), id); expression_provenance.insert(OutputKey(i, output_ordinal as u16), provenance); } } let mut predicates = Vec::new(); for (execution, compiled) in executions.iter().enumerate() { let node = compiled.original_node_index as usize; if !contracts[node].is_raster_draw() { continue; } let mesh = output_ids[&bound[node]["mesh"].producer]; if mesh_root.is_some_and(|root| root != mesh) { return Err(error( "GRAPH_SOCKET_TYPE_MISMATCH", "instance traversal has multiple mesh roots", format!("nodes[{node}].inputs.mesh"), )); } mesh_root = Some(mesh); let predicate = if let Some(binding) = bound[node].get("predicate") { expression_ids[&binding.producer] } else { let NormalizedParameters::Raster { predicate_default, .. } = params[node] else { unreachable!() }; intern( SemanticType::Bool, ExpressionOp::Literal { literal: TypedLiteral::Bool(predicate_default), }, NodeOutputRef { node: graph.nodes[node].id.clone(), socket: "predicate".into(), }, None, ) }; predicates.push(PipelinePredicatePlan { execution: execution as u32, predicate, ordinal: 0, }); } for (ordinal, predicate) in predicates.iter_mut().enumerate() { predicate.ordinal = ordinal as u32; } if expression_plan.expressions.len() > MAX_EXPRESSIONS || predicates.len() > MAX_PREDICATE_PIPELINES { return Err(error( "GRAPH_LIMIT_EXCEEDED", "instance traversal plan exceeds limits", "nodes", )); } let instance_traversal = mesh_root.map(|mesh| InstanceTraversalPlan { mesh, expressions: expression_plan, pipelines: predicates, requires_camera, }); for (ordinal, e) in executions.iter().enumerate() { for o in &e.outputs { resources[o.resource as usize].producer_execution = Some(ordinal as u32); } for access in &e.accesses { if matches!(access.mode, AccessMode::ColorResolve { .. }) { resources[access.resource as usize].producer_execution = Some(ordinal as u32); } } } let render_passes = build_render_passes(&executions, &resources, &families); let execution_pass: Vec = 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 = execution_pass[ordinal]; let mut touched = BTreeSet::new(); for x in &e.inputs { touched.insert(x.resource); } for x in &e.outputs { touched.insert(x.resource); } for x in &e.accesses { touched.insert(x.resource); } for r in touched { let life = resources[r as usize].lifetime.get_or_insert(Lifetime { first_use: ordinal, last_use: ordinal, }); life.first_use = life.first_use.min(ordinal); life.last_use = life.last_use.max(ordinal); } } for f in &mut families { let mut first = None; let mut last = 0; for v in &mut f.versions { v.lifetime = resources[v.resource as usize].lifetime.unwrap(); first = Some(first.map_or(v.lifetime.first_use, |x: u32| x.min(v.lifetime.first_use))); last = last.max(v.lifetime.last_use); } f.lifetime = Lifetime { first_use: first.unwrap_or(0), last_use: last, }; f.usage = texture_usage(f, &executions); let transient = match f.source { TextureFamilySource::AuthoredTexture { residency, .. } => { residency == TextureResidency::Transient } TextureFamilySource::CompilerDefaultInput { .. } | TextureFamilySource::CompilerColorResolve { .. } => true, }; f.aliasable = transient && f.versions.iter().all(|v| v.initialized); } let (classes, transient) = allocate(&mut families, &mut resources); if resources.len() > 1024 { return Err(error( "GRAPH_LIMIT_EXCEEDED", "too many final resources", "resources", )); } Ok(CompiledGraph { schema_version: 3, graph_id: graph.graph_id, revision: graph.revision, node_count: graph.nodes.len() as u32, pipelines: graph.pipelines, resources, executions, render_passes, texture_families: families, allocation_classes: classes, culled_node_count: (graph.nodes.len() - live.len()) as u32, culled_resource_count: (all_outputs - authored_materialized_output_count) as u32, transient_slot_count: transient, instance_traversal, }) } pub(crate) fn execution_attachments( execution: &CompiledExecution, ) -> (Vec, Option) { 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 { 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 = 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("color") }) && depth.as_ref().is_none_or(|_| { previous.outputs.iter().any(|out| { out.socket == "depth" && Some(out.resource) == target_input("depth") }) }); 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 { depth_or_array_layers, .. } | NormalizedTextureExtent::SurfaceRelative { depth_or_array_layers, .. } => *depth_or_array_layers, } } pub(super) fn family_descriptor(family: &TextureFamily) -> &NormalizedTextureDescriptor { match &family.source { TextureFamilySource::AuthoredTexture { descriptor, .. } | TextureFamilySource::CompilerDefaultInput { descriptor, .. } | TextureFamilySource::CompilerColorResolve { descriptor, .. } => descriptor, } } 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 && descriptor.view_formats.is_empty() } pub(super) fn frame_out_source_compatible( descriptor: &NormalizedTextureDescriptor, dynamic_range: &FrameDynamicRange, ) -> bool { is_single_view_d2(descriptor) && match dynamic_range { FrameDynamicRange::Hdr { .. } => descriptor.format == TextureFormat::Rgba16Float, FrameDynamicRange::Sdr => matches!( descriptor.format, TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm | TextureFormat::Rgba16Float ), } } pub(super) fn texture_usage( f: &TextureFamily, executions: &[CompiledExecution], ) -> Vec { let rs: HashSet<_> = f.versions.iter().map(|v| v.resource).collect(); let mut u = BTreeSet::new(); for e in executions { for a in &e.accesses { if !rs.contains(&a.resource) { continue; } match a.mode { AccessMode::SampledTexture => { u.insert(TextureUsage::Sampled); } AccessMode::StorageRead | AccessMode::StorageWrite { .. } => { u.insert(TextureUsage::Storage); } AccessMode::ColorAttachment { .. } => { u.insert(TextureUsage::ColorAttachment); } AccessMode::DepthAttachment { .. } => { u.insert(TextureUsage::DepthAttachment); } AccessMode::ColorResolve { .. } => { u.insert(TextureUsage::ColorAttachment); } _ => {} } } } u.into_iter().collect() } fn allocate( families: &mut [TextureFamily], resources: &mut [CompiledResource], ) -> (Vec, u32) { let mut grouped: BTreeMap> = BTreeMap::new(); for (i, f) in families.iter().enumerate() { let descriptor = family_descriptor(f); 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; for (key, ids) in grouped { let class = classes.len() as u32; let mut slots: Vec = Vec::new(); let mut aliasable = Vec::new(); let mut dedicated = Vec::new(); let mut persistent_ids = Vec::new(); for fi in ids { let persistent = matches!( families[fi].source, TextureFamilySource::AuthoredTexture { residency: TextureResidency::Persistent, .. } ); let alias = families[fi].aliasable && !persistent; if persistent { persistent_ids.push(fi); } else if alias { aliasable.push(fi); } else { dedicated.push(fi); } } aliasable.sort_by_key(|&fi| { ( families[fi].lifetime.first_use, families[fi].lifetime.last_use, families[fi].key.clone(), ) }); dedicated.sort_by_key(|&fi| families[fi].key.clone()); persistent_ids.sort_by_key(|&fi| families[fi].key.clone()); for fi in aliasable.into_iter().chain(dedicated).chain(persistent_ids) { let persistent = matches!( families[fi].source, TextureFamilySource::AuthoredTexture { residency: TextureResidency::Persistent, .. } ); let alias = families[fi].aliasable && !persistent; let found = if alias { slots.iter().position(|s| { s.kind == AllocationKind::AliasedTransient && s.occupants.iter().all(|&old| { families[old as usize].lifetime.last_use < families[fi].lifetime.first_use }) }) } else { None }; let slot = found.unwrap_or_else(|| { let s = slots.len(); if !persistent { transient += 1; } slots.push(AllocationSlot { kind: if persistent { AllocationKind::Persistent } else if alias { AllocationKind::AliasedTransient } else { AllocationKind::DedicatedTransient }, usage: Vec::new(), occupants: Vec::new(), }); s }); slots[slot].occupants.push(fi as u32); slots[slot].usage.extend(families[fi].usage.iter().copied()); slots[slot].usage.sort(); slots[slot].usage.dedup(); let a = AllocationRef { class, slot: slot as u32, }; families[fi].allocation = Some(a); for v in &families[fi].versions { if let ResourcePlan::Texture { allocation, .. } = &mut resources[v.resource as usize].plan { *allocation = Some(a); } } } classes.push(AllocationClass { key, slots }); } (classes, transient) }