Files
yawn/renderer/src/render_graph/compiler.rs
T
Ampandheaust 6bbf8039e4 Strip core to render data and render graphs
Move glTF, picking, camera controls, and conventional handles into addons. Keep camera and material mutations in SIMD-aligned shared SOA rows and synchronize material updates directly into GPU buffers.

Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-08-19 09:41:41 +00:00

3359 lines
125 KiB
Rust

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<f32, GraphError> {
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<const N: usize>(
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<OutputKey, (u32, u32, u32)>,
resolved: &HashMap<OutputKey, ResolvedTransition>,
authored_families: &'a [TextureFamily],
drafts: &'a [DefaultDraft],
) -> Result<SampledDescriptor<'a>, 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<usize>],
ordering_outgoing: &[Vec<usize>],
edges: &[DependencyEdge],
live: &HashSet<usize>,
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<String>) -> GraphError {
GraphError::at(code, message, path)
}
fn validate_name_length(s: &str, path: impl Into<String>) -> 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<String>) -> 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<CompiledGraph, GraphError> {
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<NormalizedTextureDescriptor, GraphError> {
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<NormalizedParameters, GraphError> {
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::<Empty>(node.parameters.clone()).map_err(invalid)?;
$variant
}};
}
fn literal(value: &serde_json::Value, ty: SemanticType) -> Option<TypedLiteral> {
let floats = |value: &serde_json::Value, n: usize| -> Option<Vec<f32>> {
let values = value.as_array()?;
if values.len() != n {
return None;
}
values
.iter()
.map(|value| value.as_f64().map(|value| value as f32))
.collect::<Option<Vec<_>>>()
.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::<Option<Vec<u32>>>()?
.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::<Option<Vec<_>>>()?;
match ty {
SemanticType::Mat2 => TypedLiteral::Mat2(
columns
.into_iter()
.map(|v| v.try_into().ok())
.collect::<Option<Vec<_>>>()?
.try_into()
.ok()?,
),
SemanticType::Mat3 => TypedLiteral::Mat3(
columns
.into_iter()
.map(|v| v.try_into().ok())
.collect::<Option<Vec<_>>>()?
.try_into()
.ok()?,
),
_ => TypedLiteral::Mat4(
columns
.into_iter()
.map(|v| v.try_into().ok())
.collect::<Option<Vec<_>>>()?
.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<CompiledGraph, GraphError> {
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::<usize>());
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::<Result<_, _>>()?;
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::<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 && 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<BTreeMap<&str, BoundInput>> = 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::<OutputKey, Vec<usize>>::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<AttachmentRoot, GraphError> {
if let Some(&root) = roots.get(&(node, ordinal)) {
return Ok(root);
}
if colors.get(&(node, ordinal)) == Some(&1) {
return Err(error(
"GRAPH_ATTACHMENT_LINEAGE_INVALID",
"attachment lineage is cyclic",
format!("nodes[{node}].inputs"),
));
}
colors.insert((node, ordinal), 1);
let socket = if ordinal == 0 { "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::<OutputKey, OutputKey>::new();
for ordinal in 0..=1u16 {
let mut colors = HashMap::new();
let mut roots = HashMap::new();
let mut cohorts = BTreeMap::<AttachmentRoot, Vec<usize>>::new();
for &node in &raster_nodes {
let root = attachment_root(node, ordinal, &bound, &contracts, &mut colors, &mut roots)?;
cohorts.entry(root).or_default().push(node);
}
for (root, mut members) in cohorts {
members.sort_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 &params[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<DefaultDraft> = 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<TextureTransition> = Vec::new();
let mut transitions_by_target: BTreeMap<TransitionTargetKey, Vec<usize>> = 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<OutputKey, usize>,
source_family: &HashMap<TransitionTargetKey, u32>,
colors: &mut HashMap<OutputKey, u8>,
resolved: &mut HashMap<OutputKey, ResolvedTransition>,
) -> 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<OutputKey, (u32, u32, u32)> = 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::<OutputKey>::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, .. } = &params[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<usize>],
ordering_outgoing: &[Vec<usize>],
edges: &[DependencyEdge],
residual: &[bool],
colors: &mut [u8],
node_stack: &mut Vec<usize>,
edge_stack: &mut Vec<usize>,
) -> Option<Vec<usize>> {
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,
} = &params[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 &params[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::<OutputKey, ExprId>::new();
let mut expression_provenance = HashMap::<OutputKey, Option<u32>>::new();
let mut cse = HashMap::<String, ExprId>::new();
let mut requires_camera = false;
let mut mesh_root = None;
let mut intern = |semantic_type: SemanticType,
op: ExpressionOp,
origin: NodeOutputRef,
mesh_provenance: Option<u32>| {
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 &params[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<u32> = render_passes
.iter()
.enumerate()
.flat_map(|(pass, value)| value.executions.iter().map(move |_| pass as u32))
.collect();
// Dense lifetimes use physical pass ordinals; producer metadata remains logical.
for (ordinal, e) in executions.iter().enumerate() {
let ordinal = 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<ColorAttachmentPlan>, Option<DepthStencilAttachmentPlan>) {
let mut colors = Vec::new();
let mut depth = None;
for access in &execution.accesses {
match access.mode {
AccessMode::ColorAttachment {
location,
load,
store,
..
} => colors.push(ColorAttachmentPlan {
resource: access.resource,
resolve_target: execution.accesses.iter().find_map(|candidate| {
match candidate.mode {
AccessMode::ColorResolve {
source,
location: l,
} if source == access.resource && l == location => Some(candidate.resource),
_ => None,
}
}),
location,
load,
store,
}),
AccessMode::DepthAttachment { load, store, .. } => {
depth = Some(DepthStencilAttachmentPlan {
resource: access.resource,
load,
store,
})
}
_ => {}
}
}
colors.sort_by_key(|color| color.location);
(colors, depth)
}
pub(crate) fn build_render_passes(
executions: &[CompiledExecution],
resources: &[CompiledResource],
families: &[TextureFamily],
) -> Vec<PhysicalRenderPass> {
let family = |resource: u32| {
resources
.get(resource as usize)
.and_then(|resource| match resource.plan {
ResourcePlan::Texture { family, .. }
| ResourcePlan::TextureSource { family, .. } => Some(family),
_ => None,
})
};
let mut passes: Vec<PhysicalRenderPass> = Vec::new();
for (index, execution) in executions.iter().enumerate() {
if matches!(execution.kind, ExecutionKind::FrameOut { .. }) {
passes.push(PhysicalRenderPass {
executions: vec![index as u32],
kind: PhysicalRenderPassKind::Surface,
});
continue;
}
let (colors, depth) = execution_attachments(execution);
let mut merged = false;
if matches!(execution.kind, ExecutionKind::RasterDraw)
&& colors.iter().all(|v| v.load == NormalizedColorLoad::Load)
&& depth
.as_ref()
.is_none_or(|v| v.load == NormalizedDepthLoad::Load)
{
if let Some(PhysicalRenderPass {
executions: members,
kind:
PhysicalRenderPassKind::Texture {
color_attachments: previous_colors,
depth_stencil: previous_depth,
},
}) = passes.last_mut()
{
let previous = &executions[*members.last().unwrap() as usize];
let target_input = |socket: &str| {
execution
.inputs
.iter()
.find(|input| input.socket == socket)
.map(|input| input.resource)
};
let exact = previous.outputs.iter().any(|out| {
out.socket == "color" && Some(out.resource) == target_input("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<TextureUsage> {
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<AllocationClass>, u32) {
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = 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<AllocationSlot> = 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)
}