feat: synthesize default graph attachments
Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -136,6 +136,14 @@ fn color(value: [f32; 4], min: f32, max: f32, base: &str) -> Result<[f32; 3], Gr
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct OutputKey(usize, u16);
|
struct OutputKey(usize, u16);
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
enum TransitionTargetKey {
|
||||||
|
Authored(OutputKey),
|
||||||
|
CompilerDefaultInput {
|
||||||
|
owner_node: usize,
|
||||||
|
input_ordinal: u16,
|
||||||
|
},
|
||||||
|
}
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct BoundInput {
|
struct BoundInput {
|
||||||
producer: OutputKey,
|
producer: OutputKey,
|
||||||
@@ -155,7 +163,7 @@ struct DependencyEdge {
|
|||||||
struct TextureTransition {
|
struct TextureTransition {
|
||||||
writer_node: usize,
|
writer_node: usize,
|
||||||
input_socket: &'static str,
|
input_socket: &'static str,
|
||||||
target: OutputKey,
|
target: TransitionTargetKey,
|
||||||
output: OutputKey,
|
output: OutputKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +172,74 @@ enum ResolvedTransition {
|
|||||||
Resolved {
|
Resolved {
|
||||||
family: u32,
|
family: u32,
|
||||||
version: u32,
|
version: u32,
|
||||||
target: OutputKey,
|
target: TransitionTargetKey,
|
||||||
},
|
},
|
||||||
Cyclic,
|
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(
|
fn reaches(
|
||||||
from: usize,
|
from: usize,
|
||||||
to: usize,
|
to: usize,
|
||||||
@@ -1037,7 +1108,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let all_outputs: usize = contracts.iter().map(|c| c.outputs.len()).sum();
|
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.
|
// Establish families and transitions without relying on a schedule.
|
||||||
let mut families = Vec::new();
|
let mut families = Vec::new();
|
||||||
@@ -1054,7 +1130,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
} => {
|
} => {
|
||||||
let id = families.len() as u32;
|
let id = families.len() as u32;
|
||||||
let r = source.unwrap();
|
let r = source.unwrap();
|
||||||
source_family.insert(OutputKey(i, 0), id);
|
source_family.insert(TransitionTargetKey::Authored(OutputKey(i, 0)), id);
|
||||||
families.push(TextureFamily {
|
families.push(TextureFamily {
|
||||||
id,
|
id,
|
||||||
key: TextureFamilyKey {
|
key: TextureFamilyKey {
|
||||||
@@ -1079,8 +1155,72 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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].key != "pipeline" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (input_ordinal, (socket, role, format, opposite)) in [
|
||||||
|
(
|
||||||
|
"colorTarget",
|
||||||
|
CompilerTextureRole::ColorTarget,
|
||||||
|
TextureFormat::Rgba16Float,
|
||||||
|
"depthTarget",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"depthTarget",
|
||||||
|
CompilerTextureRole::DepthTarget,
|
||||||
|
TextureFormat::Depth32Float,
|
||||||
|
"colorTarget",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if bound[i].contains_key(socket) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let input_ordinal = input_ordinal as u16 + 2;
|
||||||
|
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: Vec<TextureTransition> = Vec::new();
|
||||||
let mut transitions_by_target: BTreeMap<OutputKey, Vec<usize>> = BTreeMap::new();
|
let mut transitions_by_target: BTreeMap<TransitionTargetKey, Vec<usize>> = BTreeMap::new();
|
||||||
for i in 0..graph.nodes.len() {
|
for i in 0..graph.nodes.len() {
|
||||||
if !live.contains(&i) {
|
if !live.contains(&i) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1094,7 +1234,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
let transition = TextureTransition {
|
let transition = TextureTransition {
|
||||||
writer_node: i,
|
writer_node: i,
|
||||||
input_socket,
|
input_socket,
|
||||||
target: bound[i][input_socket].producer,
|
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),
|
output: OutputKey(i, output_ordinal),
|
||||||
};
|
};
|
||||||
let index = transitions.len();
|
let index = transitions.len();
|
||||||
@@ -1110,7 +1253,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
output: OutputKey,
|
output: OutputKey,
|
||||||
transitions: &[TextureTransition],
|
transitions: &[TextureTransition],
|
||||||
transition_for_output: &HashMap<OutputKey, usize>,
|
transition_for_output: &HashMap<OutputKey, usize>,
|
||||||
source_family: &HashMap<OutputKey, u32>,
|
source_family: &HashMap<TransitionTargetKey, u32>,
|
||||||
colors: &mut HashMap<OutputKey, u8>,
|
colors: &mut HashMap<OutputKey, u8>,
|
||||||
resolved: &mut HashMap<OutputKey, ResolvedTransition>,
|
resolved: &mut HashMap<OutputKey, ResolvedTransition>,
|
||||||
) -> ResolvedTransition {
|
) -> ResolvedTransition {
|
||||||
@@ -1128,9 +1271,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
version: 0,
|
version: 0,
|
||||||
target: transition.target,
|
target: transition.target,
|
||||||
}
|
}
|
||||||
} else if transition_for_output.contains_key(&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(
|
match resolve_transition(
|
||||||
transition.target,
|
target_output,
|
||||||
transitions,
|
transitions,
|
||||||
transition_for_output,
|
transition_for_output,
|
||||||
source_family,
|
source_family,
|
||||||
@@ -1146,6 +1292,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
},
|
},
|
||||||
ResolvedTransition::Cyclic => ResolvedTransition::Cyclic,
|
ResolvedTransition::Cyclic => ResolvedTransition::Cyclic,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ResolvedTransition::Cyclic
|
ResolvedTransition::Cyclic
|
||||||
};
|
};
|
||||||
@@ -1179,7 +1326,16 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
target,
|
target,
|
||||||
} = resolved[&transition.output]
|
} = resolved[&transition.output]
|
||||||
{
|
{
|
||||||
let target_id = output_ids[&target];
|
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));
|
version_of.insert(transition.output, (family, version, target_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1217,7 +1373,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if !version_of.contains_key(&key) {
|
if !version_of.contains_key(&key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(next_indices) = transitions_by_target.get(&key) else {
|
let Some(next_indices) = transitions_by_target.get(&TransitionTargetKey::Authored(key))
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let [next_index] = next_indices.as_slice() else {
|
let [next_index] = next_indices.as_slice() else {
|
||||||
@@ -1249,7 +1406,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if contracts[i].key == "pipeline" {
|
if contracts[i].key == "pipeline" {
|
||||||
if bound[i]["colorTarget"].producer == bound[i]["depthTarget"].producer
|
if bound[i].get("colorTarget").map(|b| b.producer)
|
||||||
|
== bound[i].get("depthTarget").map(|b| b.producer)
|
||||||
|
&& bound[i].contains_key("colorTarget")
|
||||||
|| matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df)
|
|| matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df)
|
||||||
{
|
{
|
||||||
return Err(error(
|
return Err(error(
|
||||||
@@ -1286,32 +1445,48 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Materialize versions only after hazard and writer precedence has been settled.
|
// Infer draft descriptors without recursion. Unknown and invalid dependencies
|
||||||
for transition in &transitions {
|
// remain deferred so descriptor diagnostics never steal cycle diagnostics.
|
||||||
if let Some(&(family, version, target)) = version_of.get(&transition.output) {
|
for _ in 0..default_roots.len() {
|
||||||
families[family as usize].versions.push(TextureVersion {
|
let mut changed = false;
|
||||||
version,
|
for index in 0..default_roots.len() {
|
||||||
resource: output_ids[&transition.output],
|
if matches!(default_roots[index].descriptor, DraftDescriptor::Known(_)) {
|
||||||
target,
|
continue;
|
||||||
initialized: true,
|
}
|
||||||
stored: true,
|
let owner = default_roots[index].owner_node;
|
||||||
lifetime: Lifetime {
|
let opposite_output = if default_roots[index].role == CompilerTextureRole::ColorTarget {
|
||||||
first_use: 0,
|
OutputKey(owner, 1)
|
||||||
last_use: 0,
|
} else {
|
||||||
},
|
OutputKey(owner, 0)
|
||||||
|
};
|
||||||
|
let Some(&(opposite_family, _, _)) = version_of.get(&opposite_output) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
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 {
|
||||||
|
known_family_descriptor(opposite_family, &families, &default_roots)
|
||||||
|
.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: 1,
|
||||||
|
view_formats: vec![],
|
||||||
});
|
});
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for family in &mut families {
|
if !changed {
|
||||||
family.versions.sort_by_key(|version| version.version);
|
break;
|
||||||
for (index, version) in family.versions.iter().enumerate() {
|
|
||||||
if version.version != index as u32 {
|
|
||||||
return Err(error(
|
|
||||||
"GRAPH_RESOURCE_VERSION_INVALID",
|
|
||||||
"texture versions must form a dense linear chain",
|
|
||||||
"resources",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,25 +1495,47 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if !live.contains(&i) || contracts[i].key != "pipeline" {
|
if !live.contains(&i) || contracts[i].key != "pipeline" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (Some(&(cf, _, _)), Some(&(df, _, _))) = (
|
let cd = version_of
|
||||||
version_of.get(&OutputKey(i, 0)),
|
.get(&OutputKey(i, 0))
|
||||||
version_of.get(&OutputKey(i, 1)),
|
.and_then(|&(family, _, _)| known_family_descriptor(family, &families, &default_roots));
|
||||||
) else {
|
let dd = version_of
|
||||||
continue;
|
.get(&OutputKey(i, 1))
|
||||||
};
|
.and_then(|&(family, _, _)| known_family_descriptor(family, &families, &default_roots));
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor: cd, .. } =
|
let ok_depth = dd.is_none_or(|dd| {
|
||||||
&families[cf as usize].source;
|
dd.dimension == TextureDimension::D2
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor: dd, .. } =
|
|
||||||
&families[df as usize].source;
|
|
||||||
let ok_depth = dd.dimension == TextureDimension::D2
|
|
||||||
&& dd.format == TextureFormat::Depth32Float
|
&& dd.format == TextureFormat::Depth32Float
|
||||||
&& dd.sample_count == 1
|
&& dd.sample_count == 1
|
||||||
&& extent_layers(&dd.extent) == 1;
|
&& dd.mip_level_count == 1
|
||||||
let ok_color = cd.format != TextureFormat::Depth32Float
|
&& dd.view_formats.is_empty()
|
||||||
&& cd.dimension == dd.dimension
|
&& extent_layers(&dd.extent) == 1
|
||||||
&& cd.extent == dd.extent
|
});
|
||||||
&& cd.sample_count == 1;
|
let ok_color = cd.is_none_or(|cd| {
|
||||||
if !ok_depth || !ok_color {
|
cd.dimension == TextureDimension::D2
|
||||||
|
&& cd.format != TextureFormat::Depth32Float
|
||||||
|
&& cd.sample_count == 1
|
||||||
|
&& 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.colorTarget"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !ok_depth {
|
||||||
|
return Err(error(
|
||||||
|
"GRAPH_ILLEGAL_ACCESS",
|
||||||
|
"depth attachment is invalid",
|
||||||
|
format!("nodes[{i}].inputs.depthTarget"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
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(
|
return Err(error(
|
||||||
"GRAPH_ILLEGAL_ACCESS",
|
"GRAPH_ILLEGAL_ACCESS",
|
||||||
"attachments are incompatible",
|
"attachments are incompatible",
|
||||||
@@ -1346,77 +1543,99 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i in 0..graph.nodes.len() {
|
for i in 0..graph.nodes.len() {
|
||||||
if !live.contains(&i) || contracts[i].fullscreen_policy.is_none() {
|
if !live.contains(&i) || contracts[i].fullscreen_policy.is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let source_key = bound[i]["source"].producer;
|
let source = sampled_descriptor(
|
||||||
let Some(&(source_family_id, _, _)) = version_of.get(&source_key) else {
|
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)
|
||||||
|
});
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
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(
|
return Err(error(
|
||||||
"GRAPH_UNINITIALIZED_RESOURCE",
|
"GRAPH_UNINITIALIZED_RESOURCE",
|
||||||
"copy source is not produced",
|
"copy source is not produced",
|
||||||
format!("nodes[{i}].inputs.source"),
|
format!("nodes[{i}].inputs.source"),
|
||||||
));
|
));
|
||||||
};
|
}
|
||||||
let Some(&(target_family_id, _, _)) = version_of.get(&OutputKey(i, 0)) else {
|
if matches!(bloom, Some(SampledDescriptor::Unproduced)) {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let source_descriptor = match &families[source_family_id as usize].source {
|
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => descriptor,
|
|
||||||
};
|
|
||||||
let source_ok = source_descriptor.format == TextureFormat::Rgba16Float
|
|
||||||
&& is_single_view_d2(source_descriptor);
|
|
||||||
let target_descriptor = match &families[target_family_id as usize].source {
|
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
|
||||||
};
|
|
||||||
let authored_target_ok = target_descriptor.is_some_and(|descriptor| {
|
|
||||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
|
||||||
});
|
|
||||||
let bloom_input_ok = if contracts[i].fullscreen_policy
|
|
||||||
== Some(FullscreenPolicy::BloomComposite)
|
|
||||||
{
|
|
||||||
let bloom_key = bound[i]["bloom"].producer;
|
|
||||||
let Some(&(bloom_family_id, _, _)) = version_of.get(&bloom_key) else {
|
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_UNINITIALIZED_RESOURCE",
|
"GRAPH_UNINITIALIZED_RESOURCE",
|
||||||
"bloom source is not produced",
|
"bloom source is not produced",
|
||||||
format!("nodes[{i}].inputs.bloom"),
|
format!("nodes[{i}].inputs.bloom"),
|
||||||
));
|
));
|
||||||
};
|
|
||||||
match &families[bloom_family_id as usize].source {
|
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => {
|
|
||||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
let source_is_full_surface = matches!(&source_descriptor.extent, NormalizedTextureExtent::SurfaceRelative { width, height, depth_or_array_layers: 1 } if *width == Ratio { numerator:1, denominator:1 } && *height == Ratio { numerator:1, denominator:1 });
|
|
||||||
let target_matches_source = target_descriptor
|
|
||||||
.is_some_and(|descriptor| descriptor.extent == source_descriptor.extent);
|
|
||||||
let descriptor_ok = match contracts[i].fullscreen_policy {
|
|
||||||
Some(FullscreenPolicy::Copy) => {
|
|
||||||
target_descriptor.is_none() && source_is_full_surface
|
|
||||||
|| target_descriptor.is_some_and(|descriptor| {
|
|
||||||
descriptor.format != TextureFormat::Depth32Float
|
|
||||||
&& is_single_view_d2(descriptor)
|
|
||||||
&& descriptor.extent == source_descriptor.extent
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Some(FullscreenPolicy::BloomExtract) => authored_target_ok,
|
|
||||||
Some(FullscreenPolicy::HdrSameExtent) => authored_target_ok && target_matches_source,
|
|
||||||
Some(FullscreenPolicy::BloomComposite) => {
|
|
||||||
authored_target_ok && target_matches_source && bloom_input_ok
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if !source_ok || !descriptor_ok {
|
|
||||||
return Err(error(
|
|
||||||
"GRAPH_ILLEGAL_ACCESS",
|
|
||||||
"fullscreen textures are incompatible",
|
|
||||||
format!("nodes[{i}].inputs"),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1436,8 +1655,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
|
let Some(descriptor) = known_family_descriptor(family, &families, &default_roots) else {
|
||||||
&families[family as usize].source;
|
continue;
|
||||||
|
};
|
||||||
let NormalizedParameters::FrameOut { dynamic_range, .. } = ¶ms[i] else {
|
let NormalizedParameters::FrameOut { dynamic_range, .. } = ¶ms[i] else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
@@ -1569,6 +1789,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if transitions
|
if transitions
|
||||||
.iter()
|
.iter()
|
||||||
.any(|transition| matches!(resolved[&transition.output], ResolvedTransition::Cyclic))
|
.any(|transition| matches!(resolved[&transition.output], ResolvedTransition::Cyclic))
|
||||||
|
|| default_roots
|
||||||
|
.iter()
|
||||||
|
.any(|draft| matches!(draft.descriptor, DraftDescriptor::Deferred))
|
||||||
{
|
{
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_RESOURCE_VERSION_INVALID",
|
"GRAPH_RESOURCE_VERSION_INVALID",
|
||||||
@@ -1577,6 +1800,64 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
let mut resources = Vec::new();
|
||||||
for (i, o, out) in resource_meta {
|
for (i, o, out) in resource_meta {
|
||||||
let key = OutputKey(i, o);
|
let key = OutputKey(i, o);
|
||||||
@@ -1589,7 +1870,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
} = ¶ms[i]
|
} = ¶ms[i]
|
||||||
{
|
{
|
||||||
ResourcePlan::TextureSource {
|
ResourcePlan::TextureSource {
|
||||||
family: source_family[&key],
|
family: source_family[&TransitionTargetKey::Authored(key)],
|
||||||
residency: *residency,
|
residency: *residency,
|
||||||
descriptor: descriptor.clone(),
|
descriptor: descriptor.clone(),
|
||||||
}
|
}
|
||||||
@@ -1625,10 +1906,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
};
|
};
|
||||||
resources.push(CompiledResource {
|
resources.push(CompiledResource {
|
||||||
original_node_index: i as u32,
|
original_node_index: i as u32,
|
||||||
output_ordinal: o,
|
origin: ResourceOrigin::AuthoredOutput {
|
||||||
origin: NodeOutputRef {
|
|
||||||
node: graph.nodes[i].id.clone(),
|
node: graph.nodes[i].id.clone(),
|
||||||
socket: out.name.into(),
|
socket: out.name.into(),
|
||||||
|
output_ordinal: o,
|
||||||
},
|
},
|
||||||
semantic_type: out.semantic_type,
|
semantic_type: out.semantic_type,
|
||||||
producer_execution: None,
|
producer_execution: None,
|
||||||
@@ -1637,6 +1918,28 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
});
|
});
|
||||||
let _ = id;
|
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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
let mut executions = Vec::new();
|
let mut executions = Vec::new();
|
||||||
for &i in &order {
|
for &i in &order {
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -1647,14 +1950,26 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
let input_resource = |s: &str| output_ids[&bound[i][s].producer];
|
let input_resource = |s: &str| output_ids[&bound[i][s].producer];
|
||||||
let mut inputs = Vec::new();
|
let mut inputs = Vec::new();
|
||||||
for s in contracts[i].inputs {
|
for (input_ordinal, s) in contracts[i].inputs.iter().enumerate() {
|
||||||
if s.role != InputRole::Expression {
|
if s.role != InputRole::Expression {
|
||||||
let Some(b) = bound[i].get(s.name) else {
|
let resource = if let Some(b) = bound[i].get(s.name) {
|
||||||
|
output_ids[&b.producer]
|
||||||
|
} 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;
|
continue;
|
||||||
};
|
};
|
||||||
inputs.push(CompiledSocketInput {
|
inputs.push(CompiledSocketInput {
|
||||||
socket: s.name.into(),
|
socket: s.name.into(),
|
||||||
resource: output_ids[&b.producer],
|
resource,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2095,13 +2410,13 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
last_use: last,
|
last_use: last,
|
||||||
};
|
};
|
||||||
f.usage = texture_usage(f, &executions);
|
f.usage = texture_usage(f, &executions);
|
||||||
f.aliasable = matches!(
|
let transient = match f.source {
|
||||||
f.source,
|
TextureFamilySource::AuthoredTexture { residency, .. } => {
|
||||||
TextureFamilySource::AuthoredTexture {
|
residency == TextureResidency::Transient
|
||||||
residency: TextureResidency::Transient,
|
|
||||||
..
|
|
||||||
}
|
}
|
||||||
) && f.versions.iter().all(|v| v.initialized);
|
TextureFamilySource::CompilerDefaultInput { .. } => true,
|
||||||
|
};
|
||||||
|
f.aliasable = transient && f.versions.iter().all(|v| v.initialized);
|
||||||
}
|
}
|
||||||
let (classes, transient) = allocate(&mut families, &mut resources);
|
let (classes, transient) = allocate(&mut families, &mut resources);
|
||||||
if resources.len() > 1024 {
|
if resources.len() > 1024 {
|
||||||
@@ -2121,7 +2436,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
texture_families: families,
|
texture_families: families,
|
||||||
allocation_classes: classes,
|
allocation_classes: classes,
|
||||||
culled_node_count: (graph.nodes.len() - live.len()) as u32,
|
culled_node_count: (graph.nodes.len() - live.len()) as u32,
|
||||||
culled_resource_count: (all_outputs - output_ids.len()) as u32,
|
culled_resource_count: (all_outputs - authored_materialized_output_count) as u32,
|
||||||
transient_slot_count: transient,
|
transient_slot_count: transient,
|
||||||
instance_traversal,
|
instance_traversal,
|
||||||
})
|
})
|
||||||
@@ -2139,6 +2454,12 @@ fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
|
|||||||
} => *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, .. } => descriptor,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
|
pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
|
||||||
descriptor.dimension == TextureDimension::D2
|
descriptor.dimension == TextureDimension::D2
|
||||||
&& descriptor.sample_count == 1
|
&& descriptor.sample_count == 1
|
||||||
@@ -2195,7 +2516,7 @@ fn allocate(
|
|||||||
) -> (Vec<AllocationClass>, u32) {
|
) -> (Vec<AllocationClass>, u32) {
|
||||||
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = BTreeMap::new();
|
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = BTreeMap::new();
|
||||||
for (i, f) in families.iter().enumerate() {
|
for (i, f) in families.iter().enumerate() {
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source;
|
let descriptor = family_descriptor(f);
|
||||||
grouped
|
grouped
|
||||||
.entry(TextureCompatibilityKey {
|
.entry(TextureCompatibilityKey {
|
||||||
dimension: descriptor.dimension,
|
dimension: descriptor.dimension,
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ pub enum InputCardinality {
|
|||||||
OptionalOne,
|
OptionalOne,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum InputDefaultPolicy {
|
||||||
|
None,
|
||||||
|
ParameterLiteral,
|
||||||
|
CompilerTexture,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
#[serde(tag = "kind", content = "types", rename_all = "snake_case")]
|
#[serde(tag = "kind", content = "types", rename_all = "snake_case")]
|
||||||
pub enum TypeConstraint {
|
pub enum TypeConstraint {
|
||||||
Exact(SemanticType),
|
Exact(SemanticType),
|
||||||
@@ -67,6 +74,7 @@ pub struct InputSocketContract {
|
|||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub accepted: TypeConstraint,
|
pub accepted: TypeConstraint,
|
||||||
pub cardinality: InputCardinality,
|
pub cardinality: InputCardinality,
|
||||||
|
pub default_policy: InputDefaultPolicy,
|
||||||
pub role: InputRole,
|
pub role: InputRole,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Copy, Debug, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, serde::Serialize)]
|
||||||
@@ -97,10 +105,20 @@ const fn i(
|
|||||||
cardinality: InputCardinality,
|
cardinality: InputCardinality,
|
||||||
role: InputRole,
|
role: InputRole,
|
||||||
) -> InputSocketContract {
|
) -> InputSocketContract {
|
||||||
|
let default_policy = match cardinality {
|
||||||
|
InputCardinality::RequiredOne => InputDefaultPolicy::None,
|
||||||
|
InputCardinality::OptionalOne => match role {
|
||||||
|
InputRole::ColorTarget { .. } | InputRole::DepthTarget => {
|
||||||
|
InputDefaultPolicy::CompilerTexture
|
||||||
|
}
|
||||||
|
_ => InputDefaultPolicy::ParameterLiteral,
|
||||||
|
},
|
||||||
|
};
|
||||||
InputSocketContract {
|
InputSocketContract {
|
||||||
name,
|
name,
|
||||||
accepted: TypeConstraint::Exact(ty),
|
accepted: TypeConstraint::Exact(ty),
|
||||||
cardinality,
|
cardinality,
|
||||||
|
default_policy,
|
||||||
role,
|
role,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,10 +142,10 @@ const PIPE_I: &[InputSocketContract] = &[
|
|||||||
i(
|
i(
|
||||||
"colorTarget",
|
"colorTarget",
|
||||||
Texture,
|
Texture,
|
||||||
R,
|
O,
|
||||||
InputRole::ColorTarget { location: 0 },
|
InputRole::ColorTarget { location: 0 },
|
||||||
),
|
),
|
||||||
i("depthTarget", Texture, R, InputRole::DepthTarget),
|
i("depthTarget", Texture, O, InputRole::DepthTarget),
|
||||||
];
|
];
|
||||||
const PIPE_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
|
const PIPE_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
|
||||||
const CULL_I: &[InputSocketContract] = &[
|
const CULL_I: &[InputSocketContract] = &[
|
||||||
@@ -181,7 +199,7 @@ pub static CONTRACTS: &[Contract] = &[
|
|||||||
c!("mesh", 2, Source, NONE_I, MESH_O, false, None),
|
c!("mesh", 2, Source, NONE_I, MESH_O, false, None),
|
||||||
c!("texture", 1, Source, NONE_I, TEXTURE_O, false, None),
|
c!("texture", 1, Source, NONE_I, TEXTURE_O, false, None),
|
||||||
c!("frustum_cull", 2, Expression, CULL_I, CULL_O, false, None),
|
c!("frustum_cull", 2, Expression, CULL_I, CULL_O, false, None),
|
||||||
c!("pipeline", 2, Render, PIPE_I, PIPE_O, false, None),
|
c!("pipeline", 3, Render, PIPE_I, PIPE_O, false, None),
|
||||||
ex!("and", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
ex!("and", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||||
ex!("or", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
ex!("or", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||||
ex!("not", ins!("operand":Bool), outs!("value":Bool)),
|
ex!("not", ins!("operand":Bool), outs!("value":Bool)),
|
||||||
|
|||||||
@@ -24,14 +24,36 @@ pub struct CompiledGraph {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CompiledResource {
|
pub struct CompiledResource {
|
||||||
pub original_node_index: u32,
|
pub original_node_index: u32,
|
||||||
pub output_ordinal: u16,
|
pub origin: ResourceOrigin,
|
||||||
pub origin: NodeOutputRef,
|
|
||||||
pub semantic_type: SemanticType,
|
pub semantic_type: SemanticType,
|
||||||
pub producer_execution: Option<u32>,
|
pub producer_execution: Option<u32>,
|
||||||
pub lifetime: Option<Lifetime>,
|
pub lifetime: Option<Lifetime>,
|
||||||
pub plan: ResourcePlan,
|
pub plan: ResourcePlan,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum ResourceOrigin {
|
||||||
|
AuthoredOutput {
|
||||||
|
node: String,
|
||||||
|
socket: String,
|
||||||
|
output_ordinal: u16,
|
||||||
|
},
|
||||||
|
CompilerDefaultInput {
|
||||||
|
owner_node_index: u32,
|
||||||
|
input_ordinal: u16,
|
||||||
|
socket: String,
|
||||||
|
role: CompilerTextureRole,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CompilerTextureRole {
|
||||||
|
ColorTarget,
|
||||||
|
DepthTarget,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum ResourcePlan {
|
pub enum ResourcePlan {
|
||||||
@@ -331,7 +353,7 @@ pub struct Lifetime {
|
|||||||
pub last_use: u32,
|
pub last_use: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TextureFamilyKey {
|
pub struct TextureFamilyKey {
|
||||||
pub source_node: u32,
|
pub source_node: u32,
|
||||||
@@ -346,6 +368,13 @@ pub enum TextureFamilySource {
|
|||||||
residency: TextureResidency,
|
residency: TextureResidency,
|
||||||
descriptor: NormalizedTextureDescriptor,
|
descriptor: NormalizedTextureDescriptor,
|
||||||
},
|
},
|
||||||
|
CompilerDefaultInput {
|
||||||
|
resource: u32,
|
||||||
|
owner_node_index: u32,
|
||||||
|
input_ordinal: u16,
|
||||||
|
role: CompilerTextureRole,
|
||||||
|
descriptor: NormalizedTextureDescriptor,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
|||||||
@@ -395,7 +395,8 @@ fn texture_descriptor<'a>(
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
match &graph.texture_families.get(family as usize)?.source {
|
match &graph.texture_families.get(family as usize)?.source {
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
TextureFamilySource::AuthoredTexture { descriptor, .. }
|
||||||
|
| TextureFamilySource::CompilerDefaultInput { descriptor, .. } => Some(descriptor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,6 +683,19 @@ fn validate_fullscreen_execution(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||||
|
fn texture_family_for_resource<'a>(
|
||||||
|
graph: &'a CompiledGraph,
|
||||||
|
resource: u32,
|
||||||
|
) -> Option<&'a TextureFamily> {
|
||||||
|
let resource = graph.resources.get(resource as usize)?;
|
||||||
|
let family = match resource.plan {
|
||||||
|
ResourcePlan::TextureSource { family, .. } | ResourcePlan::Texture { family, .. } => {
|
||||||
|
family
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
graph.texture_families.get(family as usize)
|
||||||
|
}
|
||||||
for (i, resource) in graph.resources.iter().enumerate() {
|
for (i, resource) in graph.resources.iter().enumerate() {
|
||||||
if resource.semantic_type.is_virtual() {
|
if resource.semantic_type.is_virtual() {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
@@ -795,28 +809,221 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut family_keys = HashSet::new();
|
||||||
|
let mut source_claims = vec![0u8; graph.resources.len()];
|
||||||
for (fi, family) in graph.texture_families.iter().enumerate() {
|
for (fi, family) in graph.texture_families.iter().enumerate() {
|
||||||
let TextureFamilySource::AuthoredTexture {
|
if family.id as usize != fi || !family_keys.insert(family.key.clone()) {
|
||||||
resource: source,
|
return Err(invalid(
|
||||||
|
"texture family id/key is not unique and canonical",
|
||||||
|
format!("textureFamilies[{fi}]"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let (source, residency, descriptor) = match &family.source {
|
||||||
|
TextureFamilySource::AuthoredTexture {
|
||||||
|
resource,
|
||||||
residency,
|
residency,
|
||||||
descriptor,
|
descriptor,
|
||||||
} = &family.source;
|
} => (*resource, *residency, descriptor),
|
||||||
let source_resource = graph.resources.get(*source as usize).ok_or_else(|| {
|
TextureFamilySource::CompilerDefaultInput {
|
||||||
|
resource,
|
||||||
|
descriptor,
|
||||||
|
..
|
||||||
|
} => (*resource, TextureResidency::Transient, descriptor),
|
||||||
|
};
|
||||||
|
let source_resource = graph.resources.get(source as usize).ok_or_else(|| {
|
||||||
invalid(
|
invalid(
|
||||||
"texture source resource is out of bounds",
|
"texture source resource is out of bounds",
|
||||||
format!("textureFamilies[{fi}].source.resource"),
|
format!("textureFamilies[{fi}].source.resource"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
source_claims[source as usize] = source_claims[source as usize].saturating_add(1);
|
||||||
if source_resource.semantic_type != SemanticType::Texture
|
if source_resource.semantic_type != SemanticType::Texture
|
||||||
|| source_resource.producer_execution.is_some()
|
|| source_resource.producer_execution.is_some()
|
||||||
|| !matches!(&source_resource.plan, ResourcePlan::TextureSource { family: f, residency: r, descriptor: d }
|
|| !matches!(&source_resource.plan, ResourcePlan::TextureSource { family: f, residency: r, descriptor: d }
|
||||||
if *f == family.id && r == residency && d == descriptor)
|
if *f == family.id && *r == residency && d == descriptor)
|
||||||
{
|
{
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"texture family source is not canonical",
|
"texture family source is not canonical",
|
||||||
format!("textureFamilies[{fi}].source"),
|
format!("textureFamilies[{fi}].source"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let origin_ok = match (&family.source, &source_resource.origin) {
|
||||||
|
(
|
||||||
|
TextureFamilySource::AuthoredTexture { resource, .. },
|
||||||
|
ResourceOrigin::AuthoredOutput {
|
||||||
|
node: _,
|
||||||
|
socket,
|
||||||
|
output_ordinal,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
family.key.source_node == source_resource.original_node_index
|
||||||
|
&& family.key.source_socket == 0
|
||||||
|
&& *output_ordinal == 0
|
||||||
|
&& *socket == "texture"
|
||||||
|
&& *resource == source
|
||||||
|
&& source_resource.producer_execution.is_none()
|
||||||
|
}
|
||||||
|
(
|
||||||
|
TextureFamilySource::CompilerDefaultInput {
|
||||||
|
owner_node_index,
|
||||||
|
input_ordinal,
|
||||||
|
role,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
ResourceOrigin::CompilerDefaultInput {
|
||||||
|
owner_node_index: owner,
|
||||||
|
input_ordinal: input,
|
||||||
|
socket,
|
||||||
|
role: origin_role,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let owner_executions: Vec<_> = graph
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.filter(|execution| execution.original_node_index == *owner_node_index)
|
||||||
|
.collect();
|
||||||
|
let owner_input_ok = owner_executions
|
||||||
|
.as_slice()
|
||||||
|
.first()
|
||||||
|
.is_some_and(|execution| {
|
||||||
|
execution
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.filter(|input| input.socket == *socket && input.resource == source)
|
||||||
|
.count()
|
||||||
|
== 1
|
||||||
|
})
|
||||||
|
&& owner_executions[0].executor.key == "pipeline"
|
||||||
|
&& owner_executions[0].executor.version == 3
|
||||||
|
&& graph
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.flat_map(|execution| &execution.inputs)
|
||||||
|
.filter(|input| input.resource == source)
|
||||||
|
.count()
|
||||||
|
== 1
|
||||||
|
&& contract("pipeline")
|
||||||
|
.and_then(|contract| contract.inputs.get(*input_ordinal as usize))
|
||||||
|
.is_some_and(|input| {
|
||||||
|
input.name == *socket
|
||||||
|
&& matches!(
|
||||||
|
(input.role, role),
|
||||||
|
(
|
||||||
|
InputRole::ColorTarget { location: 0 },
|
||||||
|
CompilerTextureRole::ColorTarget
|
||||||
|
) | (InputRole::DepthTarget, CompilerTextureRole::DepthTarget)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
owner == owner_node_index
|
||||||
|
&& owner_executions.len() == 1
|
||||||
|
&& input == input_ordinal
|
||||||
|
&& origin_role == role
|
||||||
|
&& owner_input_ok
|
||||||
|
&& socket
|
||||||
|
== if *role == CompilerTextureRole::ColorTarget {
|
||||||
|
"colorTarget"
|
||||||
|
} else {
|
||||||
|
"depthTarget"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !origin_ok {
|
||||||
|
return Err(invalid(
|
||||||
|
"texture source origin is not canonical",
|
||||||
|
format!("resources[{source}].origin"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let TextureFamilySource::CompilerDefaultInput {
|
||||||
|
owner_node_index,
|
||||||
|
input_ordinal,
|
||||||
|
role,
|
||||||
|
descriptor,
|
||||||
|
..
|
||||||
|
} = &family.source
|
||||||
|
{
|
||||||
|
let fixed = descriptor.dimension == TextureDimension::D2
|
||||||
|
&& descriptor.format
|
||||||
|
== if *role == CompilerTextureRole::ColorTarget {
|
||||||
|
TextureFormat::Rgba16Float
|
||||||
|
} else {
|
||||||
|
TextureFormat::Depth32Float
|
||||||
|
}
|
||||||
|
&& descriptor.mip_level_count == 1
|
||||||
|
&& descriptor.sample_count == 1
|
||||||
|
&& descriptor.view_formats.is_empty()
|
||||||
|
&& matches!(
|
||||||
|
descriptor.extent,
|
||||||
|
NormalizedTextureExtent::Absolute {
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
..
|
||||||
|
} | NormalizedTextureExtent::SurfaceRelative {
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
&& family.key.source_node == *owner_node_index
|
||||||
|
&& family.key.source_socket == *input_ordinal
|
||||||
|
&& source_resource.original_node_index == *owner_node_index;
|
||||||
|
if !fixed {
|
||||||
|
return Err(invalid(
|
||||||
|
"compiler default descriptor is not canonical",
|
||||||
|
format!("textureFamilies[{fi}].source.descriptor"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let owner = graph
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.find(|execution| execution.original_node_index == *owner_node_index)
|
||||||
|
.expect("origin validation found owner");
|
||||||
|
let opposite_socket = if *role == CompilerTextureRole::ColorTarget {
|
||||||
|
"depthTarget"
|
||||||
|
} else {
|
||||||
|
"colorTarget"
|
||||||
|
};
|
||||||
|
let opposite_resource = owner
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.find(|input| input.socket == opposite_socket)
|
||||||
|
.map(|input| input.resource)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
invalid(
|
||||||
|
"compiler default opposite input is missing",
|
||||||
|
format!("executions[{owner_node_index}].inputs"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let opposite_family = texture_family_for_resource(graph, opposite_resource)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
invalid(
|
||||||
|
"compiler default opposite input is invalid",
|
||||||
|
format!("executions[{owner_node_index}].inputs"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let both_defaults = matches!(&opposite_family.source, TextureFamilySource::CompilerDefaultInput { owner_node_index: opposite_owner, .. } if opposite_owner == owner_node_index);
|
||||||
|
let expected_extent = if both_defaults {
|
||||||
|
NormalizedTextureExtent::SurfaceRelative {
|
||||||
|
width: Ratio {
|
||||||
|
numerator: 1,
|
||||||
|
denominator: 1,
|
||||||
|
},
|
||||||
|
height: Ratio {
|
||||||
|
numerator: 1,
|
||||||
|
denominator: 1,
|
||||||
|
},
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super::compiler::family_descriptor(opposite_family)
|
||||||
|
.extent
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
if descriptor.extent != expected_extent {
|
||||||
|
return Err(invalid(
|
||||||
|
"compiler default extent is not canonical",
|
||||||
|
format!("textureFamilies[{fi}].source.descriptor.extent"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
if family.versions.is_empty() {
|
if family.versions.is_empty() {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"texture family has no versions",
|
"texture family has no versions",
|
||||||
@@ -828,7 +1035,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
let mut all_initialized = true;
|
let mut all_initialized = true;
|
||||||
for (vi, version) in family.versions.iter().enumerate() {
|
for (vi, version) in family.versions.iter().enumerate() {
|
||||||
let expected_target = if vi == 0 {
|
let expected_target = if vi == 0 {
|
||||||
*source
|
source
|
||||||
} else {
|
} else {
|
||||||
family.versions[vi - 1].resource
|
family.versions[vi - 1].resource
|
||||||
};
|
};
|
||||||
@@ -870,7 +1077,7 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
format!("textureFamilies[{fi}].lifetime"),
|
format!("textureFamilies[{fi}].lifetime"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let expected_aliasable = *residency == TextureResidency::Transient && all_initialized;
|
let expected_aliasable = residency == TextureResidency::Transient && all_initialized;
|
||||||
if family.aliasable != expected_aliasable {
|
if family.aliasable != expected_aliasable {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"texture family aliasability is not canonical",
|
"texture family aliasability is not canonical",
|
||||||
@@ -878,6 +1085,15 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (ri, resource) in graph.resources.iter().enumerate() {
|
||||||
|
let is_source = matches!(resource.plan, ResourcePlan::TextureSource { .. });
|
||||||
|
if is_source != (source_claims[ri] == 1) {
|
||||||
|
return Err(invalid(
|
||||||
|
"texture source must be claimed by exactly one family",
|
||||||
|
format!("resources[{ri}].plan"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1590,7 +1806,7 @@ pub fn prepare_runtime_plan(
|
|||||||
format!("resources[{color}].plan.family"),
|
format!("resources[{color}].plan.family"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &family.source;
|
let descriptor = super::compiler::family_descriptor(family);
|
||||||
if !super::compiler::frame_out_source_compatible(descriptor, dynamic_range) {
|
if !super::compiler::frame_out_source_compatible(descriptor, dynamic_range) {
|
||||||
return Err(invalid(
|
return Err(invalid(
|
||||||
"frame_out texture descriptor is incompatible",
|
"frame_out texture descriptor is incompatible",
|
||||||
@@ -1648,6 +1864,14 @@ pub fn prepare_runtime_plan(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextureFamilySource::CompilerDefaultInput { descriptor, .. } => {
|
||||||
|
if !super::compiler::is_single_view_d2(descriptor) || family.allocation.is_none() {
|
||||||
|
return Err(invalid(
|
||||||
|
"compiler default family is not canonical",
|
||||||
|
format!("textureFamilies[{fi}]"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (vi, version) in family.versions.iter().enumerate() {
|
for (vi, version) in family.versions.iter().enumerate() {
|
||||||
if version.version as usize != vi {
|
if version.version as usize != vi {
|
||||||
@@ -1755,24 +1979,29 @@ pub fn prepare_runtime_plan(
|
|||||||
format!("allocationClasses[{ci}].slots[{si}].occupants"),
|
format!("allocationClasses[{ci}].slots[{si}].occupants"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let TextureFamilySource::AuthoredTexture {
|
let (descriptor, residency) = match &family.source {
|
||||||
|
TextureFamilySource::AuthoredTexture {
|
||||||
descriptor,
|
descriptor,
|
||||||
residency,
|
residency,
|
||||||
..
|
..
|
||||||
} = &family.source;
|
} => (descriptor, *residency),
|
||||||
|
TextureFamilySource::CompilerDefaultInput { descriptor, .. } => {
|
||||||
|
(descriptor, TextureResidency::Transient)
|
||||||
|
}
|
||||||
|
};
|
||||||
expected_usage.extend(family.usage.iter().copied());
|
expected_usage.extend(family.usage.iter().copied());
|
||||||
let kind_valid = match slot.kind {
|
let kind_valid = match slot.kind {
|
||||||
AllocationKind::AliasedTransient => {
|
AllocationKind::AliasedTransient => {
|
||||||
*residency == TextureResidency::Transient && family.aliasable
|
residency == TextureResidency::Transient && family.aliasable
|
||||||
}
|
}
|
||||||
AllocationKind::DedicatedTransient => {
|
AllocationKind::DedicatedTransient => {
|
||||||
slot.occupants.len() == 1
|
slot.occupants.len() == 1
|
||||||
&& *residency == TextureResidency::Transient
|
&& residency == TextureResidency::Transient
|
||||||
&& !family.aliasable
|
&& !family.aliasable
|
||||||
}
|
}
|
||||||
AllocationKind::Persistent => {
|
AllocationKind::Persistent => {
|
||||||
slot.occupants.len() == 1
|
slot.occupants.len() == 1
|
||||||
&& *residency == TextureResidency::Persistent
|
&& residency == TextureResidency::Persistent
|
||||||
&& !family.aliasable
|
&& !family.aliasable
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ fn texture(id: &str, format: &str) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_extent_ratio(texture: &mut Value, numerator: u32, denominator: u32) {
|
||||||
|
texture["parameters"]["texture"]["extent"]["width"] =
|
||||||
|
json!({"numerator":numerator,"denominator":denominator});
|
||||||
|
texture["parameters"]["texture"]["extent"]["height"] =
|
||||||
|
json!({"numerator":numerator,"denominator":denominator});
|
||||||
|
}
|
||||||
|
|
||||||
fn node(id: &str, key: &str, version: u32, parameters: Value, inputs: Value) -> Value {
|
fn node(id: &str, key: &str, version: u32, parameters: Value, inputs: Value) -> Value {
|
||||||
json!({ "id": id, "state": "enabled", "executor": { "key": key, "version": version },
|
json!({ "id": id, "state": "enabled", "executor": { "key": key, "version": version },
|
||||||
"parameters": parameters, "inputs": inputs })
|
"parameters": parameters, "inputs": inputs })
|
||||||
@@ -38,7 +45,7 @@ pub(crate) fn full_cull_graph() -> Value {
|
|||||||
node("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
|
node("visible", "not", 1, json!({"operandDefault":false}), json!({"operand":input("cull","isFrustumCulled")})),
|
||||||
node("class", "and", 1, json!({"leftDefault":true,"rightDefault":true}),
|
node("class", "and", 1, json!({"leftDefault":true,"rightDefault":true}),
|
||||||
json!({"left":input("bits","bit0"),"right":input("visible","value")})),
|
json!({"left":input("bits","bit0"),"right":input("visible","value")})),
|
||||||
node("pipeline", "pipeline", 2,
|
node("pipeline", "pipeline", 3,
|
||||||
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||||
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")})),
|
json!({"mesh":input("mesh","mesh"),"predicate":input("class","value"),"colorTarget":input("color","texture"),"depthTarget":input("depth","texture")})),
|
||||||
node("frame", "frame_out", 3,
|
node("frame", "frame_out", 3,
|
||||||
@@ -51,7 +58,7 @@ pub(crate) fn full_cull_graph() -> Value {
|
|||||||
#[test]
|
#[test]
|
||||||
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
fn catalog_exposes_final_mesh_pipeline_and_generic_expression_contracts() {
|
||||||
assert_eq!(contract("mesh").unwrap().version, 2);
|
assert_eq!(contract("mesh").unwrap().version, 2);
|
||||||
assert_eq!(contract("pipeline").unwrap().version, 2);
|
assert_eq!(contract("pipeline").unwrap().version, 3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contract("mesh")
|
contract("mesh")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -145,3 +152,543 @@ fn expression_provenance_rejects_cross_mesh_values() {
|
|||||||
let error = compile_value(graph).unwrap_err();
|
let error = compile_value(graph).unwrap_err();
|
||||||
assert_eq!(error.code, "GRAPH_SOCKET_TYPE_MISMATCH");
|
assert_eq!(error.code, "GRAPH_SOCKET_TYPE_MISMATCH");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn implicit_pipeline_graph() -> Value {
|
||||||
|
json!({ "schemaVersion": 2, "graphId": "implicit", "revision": 1, "nodes": [
|
||||||
|
node("mesh", "mesh", 2, json!({}), json!({})),
|
||||||
|
node("first", "pipeline", 3,
|
||||||
|
json!({"pipeline":"ground_plane","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||||
|
json!({"mesh":input("mesh","mesh")})),
|
||||||
|
node("second", "pipeline", 3,
|
||||||
|
json!({"pipeline":"gltf_standard","depthCompare":"less_equal","depthWriteEnabled":true,"clearDepth":1.0,"clearColor":[0,0,0,1],"predicateDefault":true}),
|
||||||
|
json!({"mesh":input("mesh","mesh"),"colorTarget":input("first","color"),"depthTarget":input("first","depth")})),
|
||||||
|
node("frame", "frame_out", 3,
|
||||||
|
json!({"surfaceFormat":"preferred","hdrEnabled":true,"toneMapper":"aces","exposureStops":0,"outputTransfer":"srgb","scaleMode":"stretch","filter":"linear","backgroundColor":[0,0,0,1]}),
|
||||||
|
json!({"color":input("second","color")}))
|
||||||
|
]})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contract_v3_declares_strict_default_policies() {
|
||||||
|
let pipeline = contract("pipeline").unwrap();
|
||||||
|
assert_eq!(pipeline.version, 3);
|
||||||
|
assert_eq!(pipeline.inputs[0].default_policy, InputDefaultPolicy::None);
|
||||||
|
assert_eq!(
|
||||||
|
pipeline.inputs[1].default_policy,
|
||||||
|
InputDefaultPolicy::ParameterLiteral
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pipeline.inputs[2].default_policy,
|
||||||
|
InputDefaultPolicy::CompilerTexture
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pipeline.inputs[3].default_policy,
|
||||||
|
InputDefaultPolicy::CompilerTexture
|
||||||
|
);
|
||||||
|
assert!(CONTRACTS
|
||||||
|
.iter()
|
||||||
|
.flat_map(|c| c.inputs)
|
||||||
|
.all(|input| matches!(
|
||||||
|
(input.cardinality, input.default_policy),
|
||||||
|
(InputCardinality::RequiredOne, InputDefaultPolicy::None)
|
||||||
|
| (
|
||||||
|
InputCardinality::OptionalOne,
|
||||||
|
InputDefaultPolicy::ParameterLiteral
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
InputCardinality::OptionalOne,
|
||||||
|
InputDefaultPolicy::CompilerTexture
|
||||||
|
)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disconnected_targets_have_tagged_roots_and_clear_version_zero() {
|
||||||
|
let compiled = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
let roots: Vec<_> = compiled
|
||||||
|
.resources
|
||||||
|
.iter()
|
||||||
|
.filter(|r| matches!(r.origin, ResourceOrigin::CompilerDefaultInput { .. }))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(roots.len(), 2);
|
||||||
|
assert_eq!(compiled.culled_resource_count, 0);
|
||||||
|
assert!(compiled
|
||||||
|
.texture_families
|
||||||
|
.iter()
|
||||||
|
.take(2)
|
||||||
|
.all(|f| f.aliasable));
|
||||||
|
let first = compiled
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.id == "first")
|
||||||
|
.unwrap();
|
||||||
|
let ExecutionKind::Render {
|
||||||
|
color_attachments,
|
||||||
|
depth_stencil: Some(depth),
|
||||||
|
} = &first.kind
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
color_attachments[0].load,
|
||||||
|
NormalizedColorLoad::Clear { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(depth.load, NormalizedDepthLoad::Clear { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn implicit_chain_is_deterministic_and_loads_successors() {
|
||||||
|
let a = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
let b = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&a).unwrap(),
|
||||||
|
serde_json::to_value(&b).unwrap()
|
||||||
|
);
|
||||||
|
let second = a.executions.iter().find(|e| e.id == "second").unwrap();
|
||||||
|
let ExecutionKind::Render {
|
||||||
|
color_attachments,
|
||||||
|
depth_stencil: Some(depth),
|
||||||
|
} = &second.kind
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(color_attachments[0].load, NormalizedColorLoad::Load);
|
||||||
|
assert_eq!(depth.load, NormalizedDepthLoad::Load);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_missing_attachment_copies_authored_opposite_extent() {
|
||||||
|
for missing in ["colorTarget", "depthTarget"] {
|
||||||
|
let mut graph = full_cull_graph();
|
||||||
|
graph["nodes"][8]["inputs"]
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove(missing);
|
||||||
|
let compiled = compile_value(graph).unwrap();
|
||||||
|
let default = compiled
|
||||||
|
.texture_families
|
||||||
|
.iter()
|
||||||
|
.find(|f| matches!(f.source, TextureFamilySource::CompilerDefaultInput { .. }))
|
||||||
|
.unwrap();
|
||||||
|
let explicit = compiled
|
||||||
|
.texture_families
|
||||||
|
.iter()
|
||||||
|
.find(|f| {
|
||||||
|
matches!(f.source, TextureFamilySource::AuthoredTexture { .. })
|
||||||
|
&& f.id != default.id
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
super::compiler::family_descriptor(default).extent,
|
||||||
|
super::compiler::family_descriptor(explicit).extent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_extent_follows_half_surface_and_prior_default_families() {
|
||||||
|
let mut half = full_cull_graph();
|
||||||
|
set_extent_ratio(&mut half["nodes"][0], 1, 2);
|
||||||
|
half["nodes"][8]["inputs"]
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove("depthTarget");
|
||||||
|
let compiled = compile_value(half).unwrap();
|
||||||
|
let default = compiled
|
||||||
|
.texture_families
|
||||||
|
.iter()
|
||||||
|
.find(|family| {
|
||||||
|
matches!(
|
||||||
|
family.source,
|
||||||
|
TextureFamilySource::CompilerDefaultInput { .. }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
super::compiler::family_descriptor(default).extent,
|
||||||
|
NormalizedTextureExtent::SurfaceRelative {
|
||||||
|
width: Ratio {
|
||||||
|
numerator: 1,
|
||||||
|
denominator: 2
|
||||||
|
},
|
||||||
|
height: Ratio {
|
||||||
|
numerator: 1,
|
||||||
|
denominator: 2
|
||||||
|
},
|
||||||
|
depth_or_array_layers: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let compiled = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
let defaults: Vec<_> = compiled
|
||||||
|
.texture_families
|
||||||
|
.iter()
|
||||||
|
.filter(|family| {
|
||||||
|
matches!(
|
||||||
|
family.source,
|
||||||
|
TextureFamilySource::CompilerDefaultInput { .. }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(defaults.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
super::compiler::family_descriptor(defaults[0]).extent,
|
||||||
|
super::compiler::family_descriptor(defaults[1]).extent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_attachment_diagnostics_are_socket_specific_then_mutual() {
|
||||||
|
let mut color = full_cull_graph();
|
||||||
|
color["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
||||||
|
let error = compile_value(color).unwrap_err();
|
||||||
|
assert_eq!(error.details["path"], "nodes[8].inputs.colorTarget");
|
||||||
|
|
||||||
|
let mut depth = full_cull_graph();
|
||||||
|
depth["nodes"][1]["parameters"]["texture"]["format"] = json!("rgba16_float");
|
||||||
|
let error = compile_value(depth).unwrap_err();
|
||||||
|
assert_eq!(error.details["path"], "nodes[8].inputs.depthTarget");
|
||||||
|
|
||||||
|
let mut mismatch = full_cull_graph();
|
||||||
|
set_extent_ratio(&mut mismatch["nodes"][1], 1, 2);
|
||||||
|
let error = compile_value(mismatch).unwrap_err();
|
||||||
|
assert_eq!(error.details["path"], "nodes[8].inputs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptor_dependency_cycle_keeps_graph_cycle_priority() {
|
||||||
|
let mut graph = implicit_pipeline_graph();
|
||||||
|
graph["nodes"][1]["inputs"]["depthTarget"] = input("second", "depth");
|
||||||
|
graph["nodes"][2]["inputs"]
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove("depthTarget");
|
||||||
|
assert_eq!(compile_value(graph).unwrap_err().code, "GRAPH_CYCLE");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_attachment_error_precedes_cycle() {
|
||||||
|
let mut graph = full_cull_graph();
|
||||||
|
graph["nodes"][0]["parameters"]["texture"]["format"] = json!("depth32_float");
|
||||||
|
graph["nodes"][8]["inputs"]["depthTarget"] = input("pipeline", "depth");
|
||||||
|
let error = compile_value(graph).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], "nodes[8].inputs.colorTarget");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn self_targeting_copy(source_format: &str) -> Value {
|
||||||
|
let mut graph = full_cull_graph();
|
||||||
|
graph["nodes"][0]["parameters"]["texture"]["format"] = json!(source_format);
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
9,
|
||||||
|
node(
|
||||||
|
"copy",
|
||||||
|
"fullscreen_copy",
|
||||||
|
1,
|
||||||
|
json!({}),
|
||||||
|
json!({
|
||||||
|
"source": input("pipeline", "color"),
|
||||||
|
"colorTarget": input("copy", "color")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"][10]["inputs"]["color"] = input("copy", "color");
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_invalid_fullscreen_source_precedes_target_cycle() {
|
||||||
|
let error = compile_value(self_targeting_copy("rgba8_unorm")).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], "nodes[9].inputs");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
compile_value(self_targeting_copy("rgba16_float"))
|
||||||
|
.unwrap_err()
|
||||||
|
.code,
|
||||||
|
"GRAPH_CYCLE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_invalid_fullscreen_target_precedes_source_cycle() {
|
||||||
|
let mut graph = implicit_pipeline_graph();
|
||||||
|
graph["nodes"][1]["inputs"]["depthTarget"] = input("second", "depth");
|
||||||
|
graph["nodes"][2]["inputs"]
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.remove("depthTarget");
|
||||||
|
graph["nodes"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert(3, texture("saturation_target", "rgba8_unorm"));
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
4,
|
||||||
|
node(
|
||||||
|
"saturation",
|
||||||
|
"saturation",
|
||||||
|
1,
|
||||||
|
json!({"saturation":1,"factor":1}),
|
||||||
|
json!({
|
||||||
|
"source": input("second", "color"),
|
||||||
|
"colorTarget": input("saturation_target", "texture")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"][5]["parameters"]["hdrEnabled"] = json!(false);
|
||||||
|
graph["nodes"][5]["inputs"]["color"] = input("saturation", "color");
|
||||||
|
let error = compile_value(graph).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], "nodes[4].inputs");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn self_targeting_composite(bloom_format: &str) -> Value {
|
||||||
|
let mut graph = full_cull_graph();
|
||||||
|
graph["nodes"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert(9, texture("bloom_target", bloom_format));
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
10,
|
||||||
|
node(
|
||||||
|
"bloom_copy",
|
||||||
|
"fullscreen_copy",
|
||||||
|
1,
|
||||||
|
json!({}),
|
||||||
|
json!({
|
||||||
|
"source": input("pipeline", "color"),
|
||||||
|
"colorTarget": input("bloom_target", "texture")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
11,
|
||||||
|
node(
|
||||||
|
"composite",
|
||||||
|
"bloom_composite",
|
||||||
|
1,
|
||||||
|
json!({"intensity":1}),
|
||||||
|
json!({
|
||||||
|
"source": input("pipeline", "color"),
|
||||||
|
"bloom": input("bloom_copy", "color"),
|
||||||
|
"colorTarget": input("composite", "color")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"][12]["inputs"]["color"] = input("composite", "color");
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_invalid_bloom_input_precedes_target_cycle() {
|
||||||
|
let error = compile_value(self_targeting_composite("rgba8_unorm")).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], "nodes[11].inputs");
|
||||||
|
assert_eq!(
|
||||||
|
compile_value(self_targeting_composite("rgba16_float"))
|
||||||
|
.unwrap_err()
|
||||||
|
.code,
|
||||||
|
"GRAPH_CYCLE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cyclic_fullscreen_source(target_format: &str) -> Value {
|
||||||
|
let mut graph = full_cull_graph();
|
||||||
|
graph["nodes"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert(9, texture("copy_target", target_format));
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
10,
|
||||||
|
node(
|
||||||
|
"source_cycle",
|
||||||
|
"fullscreen_copy",
|
||||||
|
1,
|
||||||
|
json!({}),
|
||||||
|
json!({
|
||||||
|
"source": input("pipeline", "color"),
|
||||||
|
"colorTarget": input("source_cycle", "color")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
11,
|
||||||
|
node(
|
||||||
|
"copy",
|
||||||
|
"fullscreen_copy",
|
||||||
|
1,
|
||||||
|
json!({}),
|
||||||
|
json!({
|
||||||
|
"source": input("source_cycle", "color"),
|
||||||
|
"colorTarget": input("copy_target", "texture")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"][12]["inputs"]["color"] = input("copy", "color");
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_fullscreen_source_defers_uninitialized_error() {
|
||||||
|
let error = compile_value(cyclic_fullscreen_source("depth32_float")).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], "nodes[11].inputs");
|
||||||
|
assert_eq!(
|
||||||
|
compile_value(cyclic_fullscreen_source("rgba16_float"))
|
||||||
|
.unwrap_err()
|
||||||
|
.code,
|
||||||
|
"GRAPH_CYCLE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cyclic_bloom_input(target_format: &str) -> Value {
|
||||||
|
let mut graph = full_cull_graph();
|
||||||
|
graph["nodes"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert(9, texture("composite_target", target_format));
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
10,
|
||||||
|
node(
|
||||||
|
"bloom_cycle",
|
||||||
|
"fullscreen_copy",
|
||||||
|
1,
|
||||||
|
json!({}),
|
||||||
|
json!({
|
||||||
|
"source": input("pipeline", "color"),
|
||||||
|
"colorTarget": input("bloom_cycle", "color")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"].as_array_mut().unwrap().insert(
|
||||||
|
11,
|
||||||
|
node(
|
||||||
|
"composite",
|
||||||
|
"bloom_composite",
|
||||||
|
1,
|
||||||
|
json!({"intensity":1}),
|
||||||
|
json!({
|
||||||
|
"source": input("pipeline", "color"),
|
||||||
|
"bloom": input("bloom_cycle", "color"),
|
||||||
|
"colorTarget": input("composite_target", "texture")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
graph["nodes"][12]["inputs"]["color"] = input("composite", "color");
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_bloom_input_defers_uninitialized_error() {
|
||||||
|
let error = compile_value(cyclic_bloom_input("depth32_float")).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
|
||||||
|
assert_eq!(error.details["path"], "nodes[11].inputs");
|
||||||
|
assert_eq!(
|
||||||
|
compile_value(cyclic_bloom_input("rgba16_float"))
|
||||||
|
.unwrap_err()
|
||||||
|
.code,
|
||||||
|
"GRAPH_CYCLE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compiler_default_source(graph: &CompiledGraph) -> (usize, u32, u32) {
|
||||||
|
graph
|
||||||
|
.texture_families
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(family_index, family)| match family.source {
|
||||||
|
TextureFamilySource::CompilerDefaultInput {
|
||||||
|
resource,
|
||||||
|
owner_node_index,
|
||||||
|
..
|
||||||
|
} => Some((family_index, resource, owner_node_index)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_runtime_plan_invalid(graph: &CompiledGraph) {
|
||||||
|
let error = validate_activatable(graph).unwrap_err();
|
||||||
|
assert_eq!(error.code, "GRAPH_RUNTIME_PLAN_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_rejects_duplicate_compiler_default_owner_identity() {
|
||||||
|
let mut graph = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
assert!(validate_activatable(&graph).is_ok());
|
||||||
|
let (_, _, owner) = compiler_default_source(&graph);
|
||||||
|
graph
|
||||||
|
.executions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|execution| execution.executor.key == "frame_out")
|
||||||
|
.unwrap()
|
||||||
|
.original_node_index = owner;
|
||||||
|
assert_runtime_plan_invalid(&graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_rejects_coherently_retagged_authored_texture_origin() {
|
||||||
|
let mut graph = compile_value(full_cull_graph()).unwrap();
|
||||||
|
assert!(validate_activatable(&graph).is_ok());
|
||||||
|
let family = graph
|
||||||
|
.texture_families
|
||||||
|
.iter_mut()
|
||||||
|
.find(|family| matches!(family.source, TextureFamilySource::AuthoredTexture { .. }))
|
||||||
|
.unwrap();
|
||||||
|
let TextureFamilySource::AuthoredTexture { resource, .. } = family.source else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
family.key.source_node = 2;
|
||||||
|
family.key.source_socket = 1;
|
||||||
|
let source = &mut graph.resources[resource as usize];
|
||||||
|
source.original_node_index = 2;
|
||||||
|
source.origin = ResourceOrigin::AuthoredOutput {
|
||||||
|
node: "mesh".into(),
|
||||||
|
socket: "type".into(),
|
||||||
|
output_ordinal: 1,
|
||||||
|
};
|
||||||
|
assert_runtime_plan_invalid(&graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_rejects_duplicate_compiler_default_input_occurrence() {
|
||||||
|
let mut graph = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
assert!(validate_activatable(&graph).is_ok());
|
||||||
|
let (_, resource, owner) = compiler_default_source(&graph);
|
||||||
|
graph
|
||||||
|
.executions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|execution| execution.original_node_index == owner)
|
||||||
|
.unwrap()
|
||||||
|
.inputs
|
||||||
|
.push(CompiledSocketInput {
|
||||||
|
socket: "duplicate".into(),
|
||||||
|
resource,
|
||||||
|
});
|
||||||
|
assert_runtime_plan_invalid(&graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_rejects_out_of_range_opposite_default_family_without_panicking() {
|
||||||
|
let mut graph = compile_value(implicit_pipeline_graph()).unwrap();
|
||||||
|
assert!(validate_activatable(&graph).is_ok());
|
||||||
|
let (_, _, owner) = compiler_default_source(&graph);
|
||||||
|
let opposite_resource = graph
|
||||||
|
.executions
|
||||||
|
.iter()
|
||||||
|
.find(|execution| execution.original_node_index == owner)
|
||||||
|
.unwrap()
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.find(|input| input.socket == "depthTarget")
|
||||||
|
.unwrap()
|
||||||
|
.resource;
|
||||||
|
let out_of_range = graph.texture_families.len() as u32;
|
||||||
|
let ResourcePlan::TextureSource { family, .. } =
|
||||||
|
&mut graph.resources[opposite_resource as usize].plan
|
||||||
|
else {
|
||||||
|
panic!("expected opposite texture source")
|
||||||
|
};
|
||||||
|
*family = out_of_range;
|
||||||
|
assert_runtime_plan_invalid(&graph);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export const GRAPH_ID = "authored_gpu_culling";
|
export const GRAPH_ID = "authored_gpu_culling";
|
||||||
export const CATALOG_VERSION = 8;
|
export const CATALOG_VERSION = 9;
|
||||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||||
const i = (type, required = true, authoringType) => ({
|
const i = (type, required = true, authoringType, defaultPolicy = required ? "none" : "parameter_literal") => ({
|
||||||
accepted: typeof type === "string" ? exact(type) : type,
|
accepted: typeof type === "string" ? exact(type) : type,
|
||||||
required,
|
required,
|
||||||
...(authoringType ? { authoringType } : {}),
|
...(authoringType ? { authoringType } : {}),
|
||||||
|
defaultPolicy,
|
||||||
});
|
});
|
||||||
const o = (type) => ({ type });
|
const o = (type) => ({ type });
|
||||||
const expression = (inputs, outputs) => ({
|
const expression = (inputs, outputs) => ({
|
||||||
@@ -107,13 +108,13 @@ export const semanticCatalog = Object.freeze({
|
|||||||
parameters: { cameraSelection: "active" },
|
parameters: { cameraSelection: "active" },
|
||||||
},
|
},
|
||||||
pipeline: {
|
pipeline: {
|
||||||
version: 2,
|
version: 3,
|
||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
mesh: i("mesh_data"),
|
mesh: i("mesh_data"),
|
||||||
predicate: i("bool", false),
|
predicate: i("bool", false),
|
||||||
colorTarget: i("texture"),
|
colorTarget: i("texture", false, undefined, "compiler_texture"),
|
||||||
depthTarget: i("texture"),
|
depthTarget: i("texture", false, undefined, "compiler_texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture"), depth: o("texture") },
|
outputs: { color: o("texture"), depth: o("texture") },
|
||||||
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
|
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
|
||||||
@@ -427,7 +428,7 @@ export const nodeDefinitions = Object.fromEntries(
|
|||||||
n,
|
n,
|
||||||
"input",
|
"input",
|
||||||
v.authoringType ?? v.accepted.types[0],
|
v.authoringType ?? v.accepted.types[0],
|
||||||
!v.required ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
|
v.defaultPolicy === "parameter_literal" ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ const predicates = (withCulling = false) => {
|
|||||||
};
|
};
|
||||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false) => {
|
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false) => {
|
||||||
const classification = predicates(withCulling);
|
const classification = predicates(withCulling);
|
||||||
|
const explicit = colorTarget || heightScale !== 1;
|
||||||
|
const target = colorTarget || "hdr";
|
||||||
return [
|
return [
|
||||||
node("hdr", "texture", texture("rgba16_float", 1, heightScale)),
|
...(explicit && !colorTarget ? [node("hdr", "texture", texture("rgba16_float", 1, heightScale))] : []),
|
||||||
node("depth", "texture", texture("depth32_float", 1, heightScale)),
|
|
||||||
node("mesh", "mesh"),
|
node("mesh", "mesh"),
|
||||||
...classification.nodes,
|
...classification.nodes,
|
||||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
|
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), ...(explicit ? { colorTarget: input(target, "texture") } : {}) }),
|
||||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
||||||
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||||
];
|
];
|
||||||
@@ -47,20 +48,20 @@ const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1
|
|||||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
||||||
const direct = (graphId, clearColor) => graph(graphId, [
|
const direct = (graphId, clearColor) => graph(graphId, [
|
||||||
node("ldr", "texture", texture("rgba8_unorm")),
|
node("ldr", "texture", texture("rgba8_unorm")),
|
||||||
...scene("ldr", clearColor).filter((item) => item.id !== "hdr"),
|
...scene("ldr", clearColor),
|
||||||
node("frame_out", "frame_out", frameOut(false), { color: input("pbr_double", "color") }),
|
node("frame_out", "frame_out", frameOut(false), { color: input("pbr_double", "color") }),
|
||||||
]);
|
]);
|
||||||
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
|
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
|
||||||
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
|
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
|
||||||
export const hdr = graph("preset_hdr_fullscreen", [
|
export const hdr = graph("preset_hdr_fullscreen", [
|
||||||
...scene("hdr"),
|
...scene(),
|
||||||
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
||||||
]);
|
]);
|
||||||
export const culling = graph("preset_gpu_culling", (() => {
|
export const culling = graph("preset_gpu_culling", (() => {
|
||||||
return [...scene("hdr", undefined, 1, true), node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") })];
|
return [...scene(undefined, undefined, 1, true), node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") })];
|
||||||
})());
|
})());
|
||||||
const postPreset = (graphId, kind) => {
|
const postPreset = (graphId, kind) => {
|
||||||
const nodes = [...scene("hdr")];
|
const nodes = [...scene()];
|
||||||
let source = "pbr_double";
|
let source = "pbr_double";
|
||||||
if (kind === "edges") {
|
if (kind === "edges") {
|
||||||
nodes.splice(0, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
nodes.splice(0, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||||
@@ -89,7 +90,7 @@ const postPreset = (graphId, kind) => {
|
|||||||
};
|
};
|
||||||
export const tone = postPreset("preset_tone", "tone");
|
export const tone = postPreset("preset_tone", "tone");
|
||||||
const displayPreset = (id, parameters, heightScale = 1) => graph(id, [
|
const displayPreset = (id, parameters, heightScale = 1) => graph(id, [
|
||||||
...scene("hdr", undefined, heightScale),
|
...scene(undefined, undefined, heightScale),
|
||||||
node("frame_out", "frame_out", parameters, { color: input("pbr_double", "color") }),
|
node("frame_out", "frame_out", parameters, { color: input("pbr_double", "color") }),
|
||||||
]);
|
]);
|
||||||
export const contain = displayPreset("preset_contain", frameOut(false, { scaleMode: "contain", filter: "nearest", backgroundColor: [0.18, 0.18, 0.18, 0.25] }), 2);
|
export const contain = displayPreset("preset_contain", frameOut(false, { scaleMode: "contain", filter: "nearest", backgroundColor: [0.18, 0.18, 0.18, 0.25] }), 2);
|
||||||
@@ -100,7 +101,7 @@ export const bloom = postPreset("preset_bloom", "bloom");
|
|||||||
export const combined = postPreset("preset_combined", "combined");
|
export const combined = postPreset("preset_combined", "combined");
|
||||||
export const grading = graph("preset_grading", [
|
export const grading = graph("preset_grading", [
|
||||||
node("balance_hdr", "texture", texture("rgba16_float")), node("exposure_hdr", "texture", texture("rgba16_float")), node("saturation_hdr", "texture", texture("rgba16_float")), node("mixer_hdr", "texture", texture("rgba16_float")),
|
node("balance_hdr", "texture", texture("rgba16_float")), node("exposure_hdr", "texture", texture("rgba16_float")), node("saturation_hdr", "texture", texture("rgba16_float")), node("mixer_hdr", "texture", texture("rgba16_float")),
|
||||||
...scene("hdr"),
|
...scene(),
|
||||||
node("balance", "color_balance", { mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1], gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1], offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1], slope: 1, slopeColor: [1,1,1,1] }, { source: input("pbr_double", "color"), colorTarget: input("balance_hdr", "texture") }),
|
node("balance", "color_balance", { mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1], gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1], offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1], slope: 1, slopeColor: [1,1,1,1] }, { source: input("pbr_double", "color"), colorTarget: input("balance_hdr", "texture") }),
|
||||||
node("exposure", "exposure_contrast", { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 }, { source: input("balance", "color"), colorTarget: input("exposure_hdr", "texture") }),
|
node("exposure", "exposure_contrast", { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 }, { source: input("balance", "color"), colorTarget: input("exposure_hdr", "texture") }),
|
||||||
node("saturation", "saturation", { saturation: 1, factor: 1 }, { source: input("exposure", "color"), colorTarget: input("saturation_hdr", "texture") }),
|
node("saturation", "saturation", { saturation: 1, factor: 1 }, { source: input("exposure", "color"), colorTarget: input("saturation_hdr", "texture") }),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ test("production render graph composition passes fxnode's public validator", asy
|
|||||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||||
);
|
);
|
||||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||||
assert.equal(fxNodeComposition.version, 8);
|
assert.equal(fxNodeComposition.version, 9);
|
||||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 42);
|
assert.equal(Object.keys(fxNodeComposition.nodes).length, 42);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Object.values(fxNodeComposition.nodes).every(
|
Object.values(fxNodeComposition.nodes).every(
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import {
|
|||||||
CATALOG_VERSION, semanticCatalog, nodeDefinitions, descriptors,
|
CATALOG_VERSION, semanticCatalog, nodeDefinitions, descriptors,
|
||||||
} from "../static/render-graph/catalog.js";
|
} from "../static/render-graph/catalog.js";
|
||||||
|
|
||||||
test("catalog v8 exposes the final mesh, pipeline, and typed-expression contracts", () => {
|
test("catalog v9 exposes the final mesh, pipeline, and typed-expression contracts", () => {
|
||||||
assert.equal(CATALOG_VERSION, 8);
|
assert.equal(CATALOG_VERSION, 9);
|
||||||
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
||||||
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
||||||
});
|
});
|
||||||
assert.equal(semanticCatalog.mesh.version, 2);
|
assert.equal(semanticCatalog.mesh.version, 2);
|
||||||
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
||||||
assert.equal(semanticCatalog.pipeline.version, 2);
|
assert.equal(semanticCatalog.pipeline.version, 3);
|
||||||
assert.equal(semanticCatalog.pipeline.inputs.predicate.required, false);
|
assert.equal(semanticCatalog.pipeline.inputs.predicate.required, false);
|
||||||
for (const key of ["and", "xnor", "equals_f32", "greater_than_u32", "combine_vec4",
|
for (const key of ["and", "xnor", "equals_f32", "greater_than_u32", "combine_vec4",
|
||||||
"separate_mat4", "combine_u32_bits", "separate_u32x16", "separate_local_aabb"])
|
"separate_mat4", "combine_u32_bits", "separate_u32x16", "separate_local_aabb"])
|
||||||
@@ -27,10 +27,19 @@ test("current culling fixture uses type-bit predicates and final socket versions
|
|||||||
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
||||||
assert.deepEqual(byId.cull.inputs.localAabb, { node: "mesh", socket: "localAabb" });
|
assert.deepEqual(byId.cull.inputs.localAabb, { node: "mesh", socket: "localAabb" });
|
||||||
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" });
|
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" });
|
||||||
assert.equal(byId.ground.executor.version, 2);
|
assert.equal(byId.ground.executor.version, 3);
|
||||||
assert.equal(byId.ground.inputs.predicate.node, "ground_final");
|
assert.equal(byId.ground.inputs.predicate.node, "ground_final");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("compiler texture sockets expose policy metadata without literal widgets", () => {
|
||||||
|
for (const socket of ["colorTarget", "depthTarget"]) {
|
||||||
|
assert.equal(semanticCatalog.pipeline.inputs[socket].defaultPolicy, "compiler_texture");
|
||||||
|
assert.equal(nodeDefinitions.pipeline.sockets[socket].default, undefined);
|
||||||
|
}
|
||||||
|
assert.equal(semanticCatalog.pipeline.inputs.predicate.defaultPolicy, "parameter_literal");
|
||||||
|
assert.notEqual(nodeDefinitions.pipeline.sockets.predicate.default, null);
|
||||||
|
});
|
||||||
|
|
||||||
test("removed architecture is absent from the authoring catalog", () => {
|
test("removed architecture is absent from the authoring catalog", () => {
|
||||||
for (const removed of ["mesh_query", "pipeline_registry"])
|
for (const removed of ["mesh_query", "pipeline_registry"])
|
||||||
assert.equal(semanticCatalog[removed], undefined);
|
assert.equal(semanticCatalog[removed], undefined);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test("presets classify visibility and material through type.words[0] predicates"
|
|||||||
assert.deepEqual(byId.pbr_double.inputs.predicate, { node: name === "culling" ? "pbr_double_final" : "double_class", socket: "value" }, name);
|
assert.deepEqual(byId.pbr_double.inputs.predicate, { node: name === "culling" ? "pbr_double_final" : "double_class", socket: "value" }, name);
|
||||||
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
||||||
assert.deepEqual(pipeline.inputs.mesh, { node: "mesh", socket: "mesh" });
|
assert.deepEqual(pipeline.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||||
assert.equal(pipeline.executor.version, 2);
|
assert.equal(pipeline.executor.version, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -39,3 +39,15 @@ test("culling adds a local-AABB expression to each material predicate", () => {
|
|||||||
for (const id of ["ground", "pbr", "pbr_double"])
|
for (const id of ["ground", "pbr", "pbr_double"])
|
||||||
assert.equal(byId[id].inputs.predicate.node.endsWith("_final"), true);
|
assert.equal(byId[id].inputs.predicate.node.endsWith("_final"), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("implicit presets start disconnected and then chain both attachments", () => {
|
||||||
|
for (const name of ["hdr", "culling", "grading"]) {
|
||||||
|
const byId = Object.fromEntries(presets[name].nodes.map((node) => [node.id, node]));
|
||||||
|
assert.equal("colorTarget" in byId.ground.inputs, false, name);
|
||||||
|
assert.equal("depthTarget" in byId.ground.inputs, false, name);
|
||||||
|
assert.deepEqual(byId.pbr.inputs.colorTarget, { node: "ground", socket: "color" }, name);
|
||||||
|
assert.deepEqual(byId.pbr.inputs.depthTarget, { node: "ground", socket: "depth" }, name);
|
||||||
|
}
|
||||||
|
const midnight = Object.fromEntries(presets.midnight.nodes.map((node) => [node.id, node]));
|
||||||
|
assert.deepEqual(midnight.ground.inputs.colorTarget, { node: "ldr", socket: "texture" });
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user