refactor: redesign render graph nodes
Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -15,15 +15,25 @@ struct TextureParameters {
|
||||
texture: TextureDescriptor,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct DepthParameters {
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CullParameters {
|
||||
camera: ActiveCamera,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct ForwardParameters {
|
||||
struct QueryParameters {
|
||||
visible_predicate: TriStatePredicate,
|
||||
visible_default: bool,
|
||||
frustum_culled_predicate: TriStatePredicate,
|
||||
frustum_culled_default: bool,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct PipelineParameters {
|
||||
pipeline: String,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
clear_color: [f64; 4],
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
@@ -157,11 +167,12 @@ fn validate_name_grammar(s: &str, path: impl Into<String>) -> Result<(), GraphEr
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mesh_predicate_matches(predicate: TriStatePredicate, flag: bool) -> bool {
|
||||
pub fn mesh_predicate_matches(predicate: RuntimePredicate, flag: bool) -> bool {
|
||||
match predicate {
|
||||
TriStatePredicate::Any => true,
|
||||
TriStatePredicate::RequiredTrue => flag,
|
||||
TriStatePredicate::RequiredFalse => !flag,
|
||||
RuntimePredicate::Any => true,
|
||||
RuntimePredicate::RequiredTrue => flag,
|
||||
RuntimePredicate::RequiredFalse => !flag,
|
||||
RuntimePredicate::Never => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,12 +360,12 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
}};
|
||||
}
|
||||
Ok(match node.executor.key.as_str() {
|
||||
"surface_target" => empty!(NormalizedParameters::SurfaceTarget),
|
||||
"scene_table" => empty!(NormalizedParameters::SceneTable),
|
||||
"local_aabb_buffer" => empty!(NormalizedParameters::LocalAabbBuffer),
|
||||
"camera_frustum" => empty!(NormalizedParameters::CameraFrustum),
|
||||
"visibility_flags" => empty!(NormalizedParameters::VisibilityFlags),
|
||||
"frustum_cull" => empty!(NormalizedParameters::FrustumCull),
|
||||
"mesh" => empty!(NormalizedParameters::Mesh),
|
||||
"frustum_cull" => {
|
||||
let p: CullParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
NormalizedParameters::FrustumCull { camera: p.camera }
|
||||
}
|
||||
"fullscreen_copy" => empty!(NormalizedParameters::FullscreenCopy),
|
||||
"tone_map" => {
|
||||
let p: ToneMapParameters =
|
||||
@@ -402,8 +413,8 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
strength: range(p.strength, 0.0, 16.0, format!("{base}.strength"))?,
|
||||
}
|
||||
}
|
||||
"present" => empty!(NormalizedParameters::Present),
|
||||
"texture_spec" => {
|
||||
"frame_out" => empty!(NormalizedParameters::FrameOut),
|
||||
"texture" => {
|
||||
let p: TextureParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
if matches!(
|
||||
@@ -416,100 +427,86 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
format!("{base}.residency"),
|
||||
));
|
||||
}
|
||||
NormalizedParameters::TextureSpec {
|
||||
let unsupported = |suffix: &str| {
|
||||
error(
|
||||
"GRAPH_UNSUPPORTED_FEATURE",
|
||||
"texture feature is unsupported",
|
||||
format!("{base}.texture.{suffix}"),
|
||||
)
|
||||
};
|
||||
if p.texture.dimension != TextureDimension::D2 {
|
||||
return Err(unsupported("dimension"));
|
||||
}
|
||||
if p.texture.mip_level_count != 1 {
|
||||
return Err(unsupported("mipLevelCount"));
|
||||
}
|
||||
if p.texture.sample_count != 1 {
|
||||
return Err(unsupported("sampleCount"));
|
||||
}
|
||||
let depth_or_array_layers = match &p.texture.extent {
|
||||
TextureExtent::Absolute {
|
||||
depth_or_array_layers,
|
||||
..
|
||||
}
|
||||
| TextureExtent::SurfaceRelative {
|
||||
depth_or_array_layers,
|
||||
..
|
||||
} => *depth_or_array_layers,
|
||||
};
|
||||
if depth_or_array_layers != 1 {
|
||||
return Err(unsupported("extent.depthOrArrayLayers"));
|
||||
}
|
||||
NormalizedParameters::Texture {
|
||||
residency: p.residency,
|
||||
texture: normalize_texture(p.texture, &base)?,
|
||||
descriptor: normalize_texture(p.texture, &base)?,
|
||||
}
|
||||
}
|
||||
"mesh_query" => {
|
||||
let object = node.parameters.as_object().ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"parameters must be an object",
|
||||
base.clone(),
|
||||
)
|
||||
})?;
|
||||
if object.len() != 1 || !object.contains_key("filters") {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"mesh query parameters must contain only filters",
|
||||
base.clone(),
|
||||
));
|
||||
}
|
||||
let filters = object["filters"].as_array().ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"filters must be an array",
|
||||
format!("{base}.filters"),
|
||||
)
|
||||
})?;
|
||||
let mut found = [None, None];
|
||||
for (j, value) in filters.iter().enumerate() {
|
||||
let filter = value.as_object().ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"filter must be an object",
|
||||
format!("{base}.filters[{j}]"),
|
||||
)
|
||||
})?;
|
||||
if filter.len() != 2
|
||||
|| !filter.contains_key("flag")
|
||||
|| !filter.contains_key("predicate")
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"filter must contain flag and predicate",
|
||||
format!("{base}.filters[{j}]"),
|
||||
));
|
||||
}
|
||||
let flag: MeshFlag =
|
||||
serde_json::from_value(filter["flag"].clone()).map_err(|e| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
&e.to_string(),
|
||||
format!("{base}.filters[{j}].flag"),
|
||||
)
|
||||
})?;
|
||||
let predicate: TriStatePredicate =
|
||||
serde_json::from_value(filter["predicate"].clone()).map_err(|e| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
&e.to_string(),
|
||||
format!("{base}.filters[{j}].predicate"),
|
||||
)
|
||||
})?;
|
||||
let index = if flag == MeshFlag::IsVisible { 0 } else { 1 };
|
||||
if found[index].replace(predicate).is_some() {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"duplicate mesh flag",
|
||||
format!("{base}.filters[{j}].flag"),
|
||||
));
|
||||
}
|
||||
}
|
||||
if found.iter().any(Option::is_none) {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"both mesh flags are required",
|
||||
format!("{base}.filters"),
|
||||
));
|
||||
let p: QueryParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
let fold = |predicate, default, linked| match (predicate, linked, default) {
|
||||
(TriStatePredicate::Any, _, _) => RuntimePredicate::Any,
|
||||
(TriStatePredicate::RequiredTrue, true, _) => RuntimePredicate::RequiredTrue,
|
||||
(TriStatePredicate::RequiredFalse, true, _) => RuntimePredicate::RequiredFalse,
|
||||
(TriStatePredicate::RequiredTrue, false, true)
|
||||
| (TriStatePredicate::RequiredFalse, false, false) => RuntimePredicate::Any,
|
||||
_ => RuntimePredicate::Never,
|
||||
};
|
||||
let mut visible = fold(
|
||||
p.visible_predicate,
|
||||
p.visible_default,
|
||||
node.inputs.contains_key("isVisible"),
|
||||
);
|
||||
let mut culled = fold(
|
||||
p.frustum_culled_predicate,
|
||||
p.frustum_culled_default,
|
||||
node.inputs.contains_key("isFrustumCulled"),
|
||||
);
|
||||
if visible == RuntimePredicate::Never || culled == RuntimePredicate::Never {
|
||||
visible = RuntimePredicate::Never;
|
||||
culled = RuntimePredicate::Never;
|
||||
}
|
||||
NormalizedParameters::MeshQuery {
|
||||
filters: [
|
||||
NormalizedMeshFilter {
|
||||
flag: MeshFlag::IsVisible,
|
||||
predicate: found[0].unwrap(),
|
||||
},
|
||||
NormalizedMeshFilter {
|
||||
flag: MeshFlag::IsFrustumCulled,
|
||||
predicate: found[1].unwrap(),
|
||||
},
|
||||
],
|
||||
visible_predicate: visible,
|
||||
frustum_culled_predicate: culled,
|
||||
}
|
||||
}
|
||||
"depth_stencil_config" => {
|
||||
let p: DepthParameters =
|
||||
"pipeline_registry" => empty!(NormalizedParameters::PipelineRegistry),
|
||||
"pipeline" => {
|
||||
let p: PipelineParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
let valid_name = !p.pipeline.is_empty()
|
||||
&& p.pipeline.len() <= 64
|
||||
&& p.pipeline.bytes().enumerate().all(|(i, c)| {
|
||||
c == b'_' || c.is_ascii_alphanumeric() && (i > 0 || c.is_ascii_alphabetic())
|
||||
});
|
||||
if !valid_name {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"pipeline must be a 1-64 byte identifier",
|
||||
format!("{base}.pipeline"),
|
||||
));
|
||||
}
|
||||
if !p.clear_depth.is_finite() || !(0.0..=1.0).contains(&p.clear_depth) {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
@@ -517,17 +514,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
format!("{base}.clearDepth"),
|
||||
));
|
||||
}
|
||||
NormalizedParameters::DepthStencilConfig {
|
||||
config: NormalizedDepthStencil {
|
||||
depth_compare: p.depth_compare,
|
||||
depth_write_enabled: p.depth_write_enabled,
|
||||
clear_depth: p.clear_depth,
|
||||
},
|
||||
}
|
||||
}
|
||||
"legacy_forward" => {
|
||||
let p: ForwardParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
if p.clear_color.iter().any(|x| !x.is_finite()) {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
@@ -535,7 +521,11 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
format!("{base}.clearColor"),
|
||||
));
|
||||
}
|
||||
NormalizedParameters::LegacyForward {
|
||||
NormalizedParameters::Pipeline {
|
||||
pipeline: p.pipeline,
|
||||
depth_compare: p.depth_compare,
|
||||
depth_write_enabled: p.depth_write_enabled,
|
||||
clear_depth: p.clear_depth,
|
||||
clear_color: p.clear_color,
|
||||
}
|
||||
}
|
||||
@@ -569,19 +559,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
));
|
||||
}
|
||||
}
|
||||
if graph
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.executor.key == "present")
|
||||
.count()
|
||||
> 64
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_LIMIT_EXCEEDED",
|
||||
"present count exceeds 64",
|
||||
"nodes",
|
||||
));
|
||||
}
|
||||
if graph.schema_version != 2 {
|
||||
return Err(GraphError::new(
|
||||
"GRAPH_SCHEMA_UNSUPPORTED",
|
||||
@@ -673,8 +650,21 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
.enumerate()
|
||||
.map(|(i, n)| decode(n, i))
|
||||
.collect::<Result<_, _>>()?;
|
||||
if graph
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| node.executor.key == "frame_out" && node.state == NodeState::Enabled)
|
||||
.count()
|
||||
!= 1
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_EXECUTION_UNSUPPORTED",
|
||||
"exactly one frame_out is required",
|
||||
"nodes",
|
||||
));
|
||||
}
|
||||
for (i, n) in graph.nodes.iter().enumerate() {
|
||||
if n.state != NodeState::Enabled {
|
||||
if n.state != NodeState::Enabled && n.executor.key != "frame_out" {
|
||||
return Err(error(
|
||||
"GRAPH_NODE_STATE_INVALID",
|
||||
"muted nodes are unsupported",
|
||||
@@ -710,12 +700,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
for (i, n) in graph.nodes.iter().enumerate() {
|
||||
for input in contracts[i].inputs {
|
||||
let inactive = matches!(¶ms[i], NormalizedParameters::MeshQuery { filters } if filters.iter().any(|f| f.flag.input_socket() == input.name && f.predicate == TriStatePredicate::Any));
|
||||
let inactive = matches!(¶ms[i], NormalizedParameters::MeshQuery { visible_predicate, frustum_culled_predicate } if (input.name == "isVisible" && matches!(visible_predicate, RuntimePredicate::Any | RuntimePredicate::Never)) || (input.name == "isFrustumCulled" && matches!(frustum_culled_predicate, RuntimePredicate::Any | RuntimePredicate::Never)));
|
||||
if !n.inputs.contains_key(input.name) {
|
||||
if input.cardinality == InputCardinality::RequiredOne
|
||||
|| (!inactive
|
||||
&& matches!(params[i], NormalizedParameters::MeshQuery { .. })
|
||||
&& input.name != "scene")
|
||||
&& input.name != "mesh")
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_CARDINALITY",
|
||||
@@ -730,7 +720,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let mut bound: Vec<BTreeMap<&str, BoundInput>> = vec![BTreeMap::new(); graph.nodes.len()];
|
||||
for (i, n) in graph.nodes.iter().enumerate() {
|
||||
for input in contracts[i].inputs {
|
||||
let inactive = matches!(¶ms[i], NormalizedParameters::MeshQuery { filters } if filters.iter().any(|f| f.flag.input_socket() == input.name && f.predicate == TriStatePredicate::Any));
|
||||
let inactive = matches!(¶ms[i], NormalizedParameters::MeshQuery { visible_predicate, frustum_culled_predicate } if (input.name == "isVisible" && matches!(visible_predicate, RuntimePredicate::Any | RuntimePredicate::Never)) || (input.name == "isFrustumCulled" && matches!(frustum_culled_predicate, RuntimePredicate::Any | RuntimePredicate::Never)));
|
||||
let Some(r) = n.inputs.get(input.name) else {
|
||||
continue;
|
||||
};
|
||||
@@ -741,10 +731,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
.enumerate()
|
||||
.find(|(_, o)| o.name == r.socket)
|
||||
.expect("producer sockets were globally validated");
|
||||
let attachment_shape_checked_later = contracts[i].key == "legacy_forward"
|
||||
&& input.name == "depthTarget"
|
||||
&& out.semantic_type == SemanticType::SurfaceTarget;
|
||||
if !accepts(input.accepted, out.semantic_type) && !attachment_shape_checked_later {
|
||||
if !accepts(input.accepted, out.semantic_type) {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"socket type mismatch",
|
||||
@@ -782,15 +769,26 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
if !seen.insert(k.0) {
|
||||
return None;
|
||||
}
|
||||
if contracts[k.0].outputs[k.1 as usize].semantic_type == SemanticType::SceneTable {
|
||||
if contracts[k.0].key == "mesh" {
|
||||
let ordinal = contracts[k.0]
|
||||
.outputs
|
||||
.iter()
|
||||
.position(|output| output.semantic_type == SemanticType::MeshData)?;
|
||||
return Some(OutputKey(k.0, ordinal as u16));
|
||||
}
|
||||
if contracts[k.0].outputs[k.1 as usize].semantic_type == SemanticType::MeshData {
|
||||
return Some(k);
|
||||
}
|
||||
k = bound[k.0].get("scene")?.producer;
|
||||
k = bound[k.0]
|
||||
.get("mesh")
|
||||
.or_else(|| bound[k.0].get("pipelineIndices"))
|
||||
.or_else(|| bound[k.0].get("activation"))?
|
||||
.producer;
|
||||
}
|
||||
};
|
||||
for (i, c) in contracts.iter().enumerate() {
|
||||
if c.key == "frustum_cull"
|
||||
&& root(bound[i]["scene"].producer, &bound, &contracts)
|
||||
&& root(bound[i]["mesh"].producer, &bound, &contracts)
|
||||
!= root(bound[i]["localAabbs"].producer, &bound, &contracts)
|
||||
{
|
||||
return Err(error(
|
||||
@@ -799,11 +797,23 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
format!("nodes[{i}].inputs.localAabbs"),
|
||||
));
|
||||
}
|
||||
if matches!(c.key, "mesh_query" | "legacy_forward") {
|
||||
let scene = root(bound[i]["scene"].producer, &bound, &contracts);
|
||||
if matches!(c.key, "mesh_query" | "pipeline_registry" | "pipeline") {
|
||||
let scene_socket = if c.key == "pipeline_registry" {
|
||||
"pipelineIndices"
|
||||
} else {
|
||||
"mesh"
|
||||
};
|
||||
let scene = root(bound[i][scene_socket].producer, &bound, &contracts);
|
||||
for (s, b) in &bound[i] {
|
||||
if b.active
|
||||
&& matches!(*s, "isVisible" | "isFrustumCulled" | "draws")
|
||||
&& matches!(
|
||||
*s,
|
||||
"isVisible"
|
||||
| "isFrustumCulled"
|
||||
| "draws"
|
||||
| "pipelineIndices"
|
||||
| "activation"
|
||||
)
|
||||
&& root(b.producer, &bound, &contracts) != scene
|
||||
{
|
||||
return Err(error(
|
||||
@@ -853,7 +863,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let mut stack: Vec<_> = contracts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.inherently_observable)
|
||||
.filter(|(i, c)| c.inherently_observable && graph.nodes[*i].state == NodeState::Enabled)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
while let Some(i) = stack.pop() {
|
||||
@@ -862,13 +872,26 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
// IDs are independent of scheduling: original node order, then contract output order.
|
||||
// Source nodes expose only outputs that survived active-edge/liveness analysis;
|
||||
// executable nodes retain their complete output shape for runtime lowering.
|
||||
let referenced_outputs: HashSet<_> = edges
|
||||
.iter()
|
||||
.filter(|edge| live.contains(&edge.to_node))
|
||||
.map(|edge| OutputKey(edge.from_node, edge.producer_output_ordinal))
|
||||
.collect();
|
||||
let mut output_ids = BTreeMap::new();
|
||||
let mut resource_meta = Vec::new();
|
||||
for i in 0..graph.nodes.len() {
|
||||
if live.contains(&i) {
|
||||
for (o, out) in contracts[i].outputs.iter().enumerate() {
|
||||
let key = OutputKey(i, o as u16);
|
||||
if contracts[i].execution == ExecutionClass::Source
|
||||
&& !referenced_outputs.contains(&key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let id = resource_meta.len() as u32;
|
||||
output_ids.insert(OutputKey(i, o as u16), id);
|
||||
output_ids.insert(key, id);
|
||||
resource_meta.push((i, o as u16, *out));
|
||||
}
|
||||
}
|
||||
@@ -884,28 +907,10 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
let source = output_ids.get(&OutputKey(i, 0)).copied();
|
||||
match ¶ms[i] {
|
||||
NormalizedParameters::SurfaceTarget => {
|
||||
let id = families.len() as u32;
|
||||
let r = source.unwrap();
|
||||
source_family.insert(OutputKey(i, 0), id);
|
||||
families.push(TextureFamily {
|
||||
id,
|
||||
key: TextureFamilyKey {
|
||||
source_node: i as u32,
|
||||
source_socket: 0,
|
||||
},
|
||||
source: TextureFamilySource::ImportedSurface { resource: r },
|
||||
lifetime: Lifetime {
|
||||
first_use: 0,
|
||||
last_use: 0,
|
||||
},
|
||||
versions: vec![],
|
||||
usage: vec![],
|
||||
allocation: None,
|
||||
aliasable: false,
|
||||
});
|
||||
}
|
||||
NormalizedParameters::TextureSpec { residency, texture } => {
|
||||
NormalizedParameters::Texture {
|
||||
residency,
|
||||
descriptor,
|
||||
} => {
|
||||
let id = families.len() as u32;
|
||||
let r = source.unwrap();
|
||||
source_family.insert(OutputKey(i, 0), id);
|
||||
@@ -918,7 +923,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
source: TextureFamilySource::AuthoredTexture {
|
||||
resource: r,
|
||||
residency: *residency,
|
||||
descriptor: texture.clone(),
|
||||
descriptor: descriptor.clone(),
|
||||
},
|
||||
lifetime: Lifetime {
|
||||
first_use: 0,
|
||||
@@ -940,7 +945,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
continue;
|
||||
}
|
||||
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
|
||||
"legacy_forward" => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||
"pipeline" => &[("colorTarget", 0), ("depthTarget", 1)],
|
||||
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur" | "bloom_composite"
|
||||
| "luminance_edge" => &[("colorTarget", 0)],
|
||||
_ => continue,
|
||||
@@ -1066,7 +1071,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
for input in contract
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|input| matches!(input.role, InputRole::Present | InputRole::SampledTexture))
|
||||
.filter(|input| matches!(input.role, InputRole::SampledTexture))
|
||||
{
|
||||
let key = bound[i][input.name].producer;
|
||||
if !version_of.contains_key(&key) {
|
||||
@@ -1103,7 +1108,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
if !live.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
if contracts[i].key == "legacy_forward" {
|
||||
if contracts[i].key == "pipeline" {
|
||||
if bound[i]["colorTarget"].producer == bound[i]["depthTarget"].producer
|
||||
|| matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df)
|
||||
{
|
||||
@@ -1113,10 +1118,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
format!("nodes[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
} else if contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|input| matches!(input.role, InputRole::SampledTexture))
|
||||
} else if contracts[i].key != "frame_out"
|
||||
&& contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|input| matches!(input.role, InputRole::SampledTexture))
|
||||
{
|
||||
let hazard = contracts[i].inputs.iter().filter(|input| matches!(input.role, InputRole::SampledTexture)).any(|input| matches!((version_of.get(&bound[i][input.name].producer), version_of.get(&OutputKey(i, 0))), (Some((sf, _, _)), Some((tf, _, _))) if sf == tf));
|
||||
if hazard {
|
||||
@@ -1176,7 +1182,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
|
||||
// Validate every independently resolved attachment before graph cycle reporting.
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i) || contracts[i].key != "legacy_forward" {
|
||||
if !live.contains(&i) || contracts[i].key != "pipeline" {
|
||||
continue;
|
||||
}
|
||||
let (Some(&(cf, _, _)), Some(&(df, _, _))) = (
|
||||
@@ -1185,33 +1191,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let cd = match &families[cf as usize].source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
||||
_ => None,
|
||||
};
|
||||
let dd = match &families[df as usize].source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => descriptor,
|
||||
_ => {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"depth target must be authored",
|
||||
format!("nodes[{i}].inputs.depthTarget"),
|
||||
))
|
||||
}
|
||||
};
|
||||
let TextureFamilySource::AuthoredTexture { descriptor: cd, .. } =
|
||||
&families[cf as usize].source;
|
||||
let TextureFamilySource::AuthoredTexture { descriptor: dd, .. } =
|
||||
&families[df as usize].source;
|
||||
let ok_depth = dd.dimension == TextureDimension::D2
|
||||
&& dd.format == TextureFormat::Depth32Float
|
||||
&& dd.sample_count == 1
|
||||
&& extent_layers(&dd.extent) == 1;
|
||||
let ok_color = cd.is_none_or(|d| {
|
||||
d.format != TextureFormat::Depth32Float
|
||||
&& d.dimension == dd.dimension
|
||||
&& d.extent == dd.extent
|
||||
&& d.sample_count == 1
|
||||
});
|
||||
let surface_ok = cd.is_some()
|
||||
|| matches!(&dd.extent,NormalizedTextureExtent::SurfaceRelative{width,height,..} if *width==Ratio{numerator:1,denominator:1}&&*height==Ratio{numerator:1,denominator:1});
|
||||
if !ok_depth || !ok_color || !surface_ok {
|
||||
let ok_color = cd.format != TextureFormat::Depth32Float
|
||||
&& cd.dimension == dd.dimension
|
||||
&& cd.extent == dd.extent
|
||||
&& cd.sample_count == 1;
|
||||
if !ok_depth || !ok_color {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"attachments are incompatible",
|
||||
@@ -1222,6 +1214,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
|
||||
for i in 0..graph.nodes.len() {
|
||||
if !live.contains(&i)
|
||||
|| contracts[i].key == "frame_out"
|
||||
|| !contracts[i]
|
||||
.inputs
|
||||
.iter()
|
||||
@@ -1242,19 +1235,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
};
|
||||
let source_descriptor = match &families[source_family_id as usize].source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => descriptor,
|
||||
TextureFamilySource::ImportedSurface { .. } => {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"copy source must be an authored texture",
|
||||
format!("nodes[{i}].inputs.source"),
|
||||
))
|
||||
}
|
||||
};
|
||||
let source_ok = source_descriptor.format == TextureFormat::Rgba16Float
|
||||
&& is_single_view_d2(source_descriptor);
|
||||
let target_descriptor = match &families[target_family_id as usize].source {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
||||
TextureFamilySource::ImportedSurface { .. } => None,
|
||||
};
|
||||
let authored_target_ok = target_descriptor.is_some_and(|descriptor| {
|
||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
||||
@@ -1272,13 +1257,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => {
|
||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(descriptor)
|
||||
}
|
||||
TextureFamilySource::ImportedSurface { .. } => {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"bloom source must be an authored texture",
|
||||
format!("nodes[{i}].inputs.bloom"),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
true
|
||||
@@ -1295,7 +1273,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
&& descriptor.extent == source_descriptor.extent
|
||||
})
|
||||
}
|
||||
"tone_map" => target_descriptor.is_none() && source_is_full_surface,
|
||||
"tone_map" => target_descriptor.is_some_and(|descriptor| {
|
||||
descriptor.format != TextureFormat::Depth32Float
|
||||
&& descriptor.format != TextureFormat::R32Float
|
||||
&& is_single_view_d2(descriptor)
|
||||
&& descriptor.extent == source_descriptor.extent
|
||||
}),
|
||||
"bloom_extract" => authored_target_ok,
|
||||
"bloom_blur" | "luminance_edge" => authored_target_ok && target_matches_source,
|
||||
"bloom_composite" => authored_target_ok && target_matches_source && bloom_input_ok,
|
||||
@@ -1310,30 +1293,29 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialization and presentation legality are later than attachment compatibility.
|
||||
// Frame output must consume an initialized, produced, filterable color texture.
|
||||
for (i, contract) in contracts.iter().enumerate() {
|
||||
if !live.contains(&i) || contract.key != "present" {
|
||||
if !live.contains(&i) || contract.key != "frame_out" {
|
||||
continue;
|
||||
}
|
||||
let key = bound[i]["surface"].producer;
|
||||
let key = bound[i]["color"].producer;
|
||||
let Some(&(family, _, _)) = version_of.get(&key) else {
|
||||
if !matches!(resolved.get(&key), Some(ResolvedTransition::Cyclic)) {
|
||||
return Err(error(
|
||||
"GRAPH_UNINITIALIZED_RESOURCE",
|
||||
"present source is not produced",
|
||||
format!("nodes[{i}].inputs.surface"),
|
||||
"frame output source is not produced",
|
||||
format!("nodes[{i}].inputs.color"),
|
||||
));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
if !matches!(
|
||||
families[family as usize].source,
|
||||
TextureFamilySource::ImportedSurface { .. }
|
||||
) {
|
||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
|
||||
&families[family as usize].source;
|
||||
if !is_filterable_frame_color(descriptor) {
|
||||
return Err(error(
|
||||
"GRAPH_ILLEGAL_ACCESS",
|
||||
"offscreen textures cannot be presented",
|
||||
format!("nodes[{i}].inputs.surface"),
|
||||
"frame output requires a filterable single-view d2 color texture",
|
||||
format!("nodes[{i}].inputs.color"),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1463,17 +1445,18 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
for (i, o, out) in resource_meta {
|
||||
let key = OutputKey(i, o);
|
||||
let id = output_ids[&key];
|
||||
let scene = || output_ids[&root(bound[i]["scene"].producer, &bound, &contracts).unwrap()];
|
||||
let mesh = || output_ids[&root(key, &bound, &contracts).unwrap()];
|
||||
let plan = match out.semantic_type {
|
||||
SemanticType::SurfaceTarget => ResourcePlan::SurfaceTarget {
|
||||
family: source_family[&key],
|
||||
},
|
||||
SemanticType::TextureSpec => {
|
||||
if let NormalizedParameters::TextureSpec { residency, texture } = ¶ms[i] {
|
||||
ResourcePlan::TextureSpec {
|
||||
SemanticType::Texture if matches!(params[i], NormalizedParameters::Texture { .. }) => {
|
||||
if let NormalizedParameters::Texture {
|
||||
residency,
|
||||
descriptor,
|
||||
} = ¶ms[i]
|
||||
{
|
||||
ResourcePlan::TextureSource {
|
||||
family: source_family[&key],
|
||||
residency: *residency,
|
||||
descriptor: texture.clone(),
|
||||
descriptor: descriptor.clone(),
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
@@ -1490,27 +1473,20 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
allocation: None,
|
||||
}
|
||||
}
|
||||
SemanticType::SceneTable => ResourcePlan::SceneTable,
|
||||
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { scene: scene() },
|
||||
SemanticType::CameraFrustum => ResourcePlan::CameraFrustum,
|
||||
SemanticType::MeshData => ResourcePlan::MeshData,
|
||||
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { mesh: mesh() },
|
||||
SemanticType::BooleanFlagBuffer => {
|
||||
if let OutputMetadata::BooleanFlag { flag } = out.metadata {
|
||||
ResourcePlan::BooleanFlagBuffer {
|
||||
scene: scene(),
|
||||
flag,
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
SemanticType::DrawStream => ResourcePlan::DrawStream { scene: scene() },
|
||||
SemanticType::DepthStencilConfig => {
|
||||
if let NormalizedParameters::DepthStencilConfig { config } = ¶ms[i] {
|
||||
ResourcePlan::DepthStencilConfig { config: *config }
|
||||
ResourcePlan::BooleanFlagBuffer { mesh: mesh(), flag }
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
SemanticType::PipelineIndexStream => ResourcePlan::PipelineIndexStream { mesh: mesh() },
|
||||
SemanticType::PipelineActivation => ResourcePlan::PipelineActivation {
|
||||
pipeline_indices: output_ids[&bound[i]["pipelineIndices"].producer],
|
||||
},
|
||||
SemanticType::DrawStream => ResourcePlan::DrawStream { mesh: mesh() },
|
||||
};
|
||||
resources.push(CompiledResource {
|
||||
original_node_index: i as u32,
|
||||
@@ -1557,9 +1533,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let kind = match contracts[i].key {
|
||||
"frustum_cull" => {
|
||||
for (s, m) in [
|
||||
("scene", AccessMode::StorageRead),
|
||||
("mesh", AccessMode::StorageRead),
|
||||
("localAabbs", AccessMode::StorageRead),
|
||||
("frustum", AccessMode::UniformRead),
|
||||
] {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: s.into(),
|
||||
@@ -1569,7 +1544,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
let r = output_ids[&OutputKey(i, 0)];
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "flags".into(),
|
||||
socket: "isFrustumCulled".into(),
|
||||
resource: r,
|
||||
mode: AccessMode::StorageWrite {
|
||||
full_overwrite: true,
|
||||
@@ -1580,7 +1555,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
"mesh_query" => {
|
||||
for s in ["scene", "isVisible", "isFrustumCulled"] {
|
||||
for s in ["mesh", "isVisible", "isFrustumCulled"] {
|
||||
if let Some(b) = bound[i].get(s).filter(|b| b.active) {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: s.into(),
|
||||
@@ -1600,23 +1575,38 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
work: ComputeWork::MeshQuery,
|
||||
}
|
||||
}
|
||||
"legacy_forward" => {
|
||||
"pipeline_registry" => {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "pipelineIndices".into(),
|
||||
resource: input_resource("pipelineIndices"),
|
||||
mode: AccessMode::SemanticRead,
|
||||
});
|
||||
ExecutionKind::CpuPreparation
|
||||
}
|
||||
"pipeline" => {
|
||||
let color = output_ids[&OutputKey(i, 0)];
|
||||
let depth = output_ids[&OutputKey(i, 1)];
|
||||
let clear = match params[i] {
|
||||
NormalizedParameters::LegacyForward { clear_color } => clear_color,
|
||||
NormalizedParameters::Pipeline { clear_color, .. } => clear_color,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let config_node = bound[i]["depthStencil"].producer.0;
|
||||
let dc = match params[config_node] {
|
||||
NormalizedParameters::DepthStencilConfig { config } => config,
|
||||
let clear_depth = match ¶ms[i] {
|
||||
NormalizedParameters::Pipeline { clear_depth, .. } => *clear_depth,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let cl = NormalizedColorLoad::Clear { value: clear };
|
||||
let dl = NormalizedDepthLoad::Clear {
|
||||
value: dc.clear_depth,
|
||||
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
|
||||
let first_depth = version_of[&OutputKey(i, 1)].1 == 0;
|
||||
let cl = if first_color {
|
||||
NormalizedColorLoad::Clear { value: clear }
|
||||
} else {
|
||||
NormalizedColorLoad::Load
|
||||
};
|
||||
for s in ["scene", "draws"] {
|
||||
let dl = if first_depth {
|
||||
NormalizedDepthLoad::Clear { value: clear_depth }
|
||||
} else {
|
||||
NormalizedDepthLoad::Load
|
||||
};
|
||||
for s in ["mesh", "draws", "activation"] {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: s.into(),
|
||||
resource: input_resource(s),
|
||||
@@ -1634,7 +1624,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
location: 0,
|
||||
load: cl,
|
||||
store: StoreOp::Store,
|
||||
full_overwrite: true,
|
||||
full_overwrite: first_color,
|
||||
},
|
||||
});
|
||||
accesses.push(CompiledAccess {
|
||||
@@ -1643,7 +1633,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
mode: AccessMode::DepthAttachment {
|
||||
load: dl,
|
||||
store: StoreOp::Store,
|
||||
full_overwrite: true,
|
||||
full_overwrite: first_depth,
|
||||
},
|
||||
});
|
||||
ExecutionKind::Render {
|
||||
@@ -1698,14 +1688,14 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
depth_stencil: None,
|
||||
}
|
||||
}
|
||||
"present" => {
|
||||
let r = input_resource("surface");
|
||||
"frame_out" => {
|
||||
let r = input_resource("color");
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "surface".into(),
|
||||
socket: "color".into(),
|
||||
resource: r,
|
||||
mode: AccessMode::Present,
|
||||
mode: AccessMode::SampledTexture,
|
||||
});
|
||||
ExecutionKind::Present { surface: r }
|
||||
ExecutionKind::FrameOut { color: r }
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
@@ -1803,13 +1793,27 @@ fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
|
||||
} => *depth_or_array_layers,
|
||||
}
|
||||
}
|
||||
fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
|
||||
pub(super) fn is_single_view_d2(descriptor: &NormalizedTextureDescriptor) -> bool {
|
||||
descriptor.dimension == TextureDimension::D2
|
||||
&& descriptor.sample_count == 1
|
||||
&& descriptor.mip_level_count == 1
|
||||
&& extent_layers(&descriptor.extent) == 1
|
||||
}
|
||||
fn texture_usage(f: &TextureFamily, executions: &[CompiledExecution]) -> Vec<TextureUsage> {
|
||||
pub(super) fn is_filterable_frame_color(descriptor: &NormalizedTextureDescriptor) -> bool {
|
||||
is_single_view_d2(descriptor)
|
||||
&& matches!(
|
||||
descriptor.format,
|
||||
TextureFormat::Rgba8Unorm
|
||||
| TextureFormat::Rgba8UnormSrgb
|
||||
| TextureFormat::Bgra8Unorm
|
||||
| TextureFormat::Bgra8UnormSrgb
|
||||
| TextureFormat::Rgba16Float
|
||||
)
|
||||
}
|
||||
pub(super) fn texture_usage(
|
||||
f: &TextureFamily,
|
||||
executions: &[CompiledExecution],
|
||||
) -> Vec<TextureUsage> {
|
||||
let rs: HashSet<_> = f.versions.iter().map(|v| v.resource).collect();
|
||||
let mut u = BTreeSet::new();
|
||||
for e in executions {
|
||||
@@ -1842,19 +1846,18 @@ fn allocate(
|
||||
) -> (Vec<AllocationClass>, u32) {
|
||||
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = BTreeMap::new();
|
||||
for (i, f) in families.iter().enumerate() {
|
||||
if let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source {
|
||||
grouped
|
||||
.entry(TextureCompatibilityKey {
|
||||
dimension: descriptor.dimension,
|
||||
format: descriptor.format,
|
||||
extent: descriptor.extent.clone(),
|
||||
mip_level_count: descriptor.mip_level_count,
|
||||
sample_count: descriptor.sample_count,
|
||||
view_formats: descriptor.view_formats.clone(),
|
||||
})
|
||||
.or_default()
|
||||
.push(i);
|
||||
}
|
||||
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source;
|
||||
grouped
|
||||
.entry(TextureCompatibilityKey {
|
||||
dimension: descriptor.dimension,
|
||||
format: descriptor.format,
|
||||
extent: descriptor.extent.clone(),
|
||||
mip_level_count: descriptor.mip_level_count,
|
||||
sample_count: descriptor.sample_count,
|
||||
view_formats: descriptor.view_formats.clone(),
|
||||
})
|
||||
.or_default()
|
||||
.push(i);
|
||||
}
|
||||
let mut classes = Vec::new();
|
||||
let mut transient = 0;
|
||||
|
||||
@@ -3,15 +3,13 @@ use super::MeshFlag;
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SemanticType {
|
||||
SurfaceTarget,
|
||||
TextureSpec,
|
||||
MeshData,
|
||||
Texture,
|
||||
SceneTable,
|
||||
LocalAabbBuffer,
|
||||
CameraFrustum,
|
||||
BooleanFlagBuffer,
|
||||
PipelineIndexStream,
|
||||
PipelineActivation,
|
||||
DrawStream,
|
||||
DepthStencilConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
@@ -21,7 +19,7 @@ pub enum ExecutionClass {
|
||||
CpuPreparation,
|
||||
Compute,
|
||||
Render,
|
||||
Present,
|
||||
Frame,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
@@ -48,8 +46,6 @@ pub enum InputRole {
|
||||
SampledTexture,
|
||||
ColorTarget { location: u32 },
|
||||
DepthTarget,
|
||||
Present,
|
||||
Configuration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
@@ -119,48 +115,42 @@ const REQUIRED: InputCardinality = InputCardinality::RequiredOne;
|
||||
const OPTIONAL: InputCardinality = InputCardinality::OptionalOne;
|
||||
const NONE_IN: &[InputSocketContract] = &[];
|
||||
const NONE_OUT: &[OutputSocketContract] = &[];
|
||||
const SURFACE_OUT: &[OutputSocketContract] =
|
||||
&[output("surface", SurfaceTarget, OutputMetadata::None)];
|
||||
const SPEC_OUT: &[OutputSocketContract] = &[output("spec", TextureSpec, OutputMetadata::None)];
|
||||
const SCENE_OUT: &[OutputSocketContract] = &[output("scene", SceneTable, OutputMetadata::None)];
|
||||
const AABB_OUT: &[OutputSocketContract] =
|
||||
&[output("localAabbs", LocalAabbBuffer, OutputMetadata::None)];
|
||||
const FRUSTUM_OUT: &[OutputSocketContract] =
|
||||
&[output("frustum", CameraFrustum, OutputMetadata::None)];
|
||||
const VISIBLE_OUT: &[OutputSocketContract] = &[output(
|
||||
"flags",
|
||||
BooleanFlagBuffer,
|
||||
OutputMetadata::BooleanFlag {
|
||||
flag: MeshFlag::IsVisible,
|
||||
},
|
||||
)];
|
||||
const TEXTURE_OUT: &[OutputSocketContract] = &[output("texture", Texture, OutputMetadata::None)];
|
||||
const MESH_OUT: &[OutputSocketContract] = &[
|
||||
output("mesh", MeshData, OutputMetadata::None),
|
||||
output("localAabbs", LocalAabbBuffer, OutputMetadata::None),
|
||||
output(
|
||||
"isVisible",
|
||||
BooleanFlagBuffer,
|
||||
OutputMetadata::BooleanFlag {
|
||||
flag: MeshFlag::IsVisible,
|
||||
},
|
||||
),
|
||||
output("pipelineIndices", PipelineIndexStream, OutputMetadata::None),
|
||||
];
|
||||
const CULLED_OUT: &[OutputSocketContract] = &[output(
|
||||
"flags",
|
||||
"isFrustumCulled",
|
||||
BooleanFlagBuffer,
|
||||
OutputMetadata::BooleanFlag {
|
||||
flag: MeshFlag::IsFrustumCulled,
|
||||
},
|
||||
)];
|
||||
const DRAW_OUT: &[OutputSocketContract] = &[output("draws", DrawStream, OutputMetadata::None)];
|
||||
const CONFIG_OUT: &[OutputSocketContract] =
|
||||
&[output("config", DepthStencilConfig, OutputMetadata::None)];
|
||||
const FORWARD_OUT: &[OutputSocketContract] = &[
|
||||
const ACTIVATION_OUT: &[OutputSocketContract] = &[output(
|
||||
"activation",
|
||||
PipelineActivation,
|
||||
OutputMetadata::None,
|
||||
)];
|
||||
const PIPELINE_OUT: &[OutputSocketContract] = &[
|
||||
output("color", Texture, OutputMetadata::None),
|
||||
output("depth", Texture, OutputMetadata::None),
|
||||
];
|
||||
const FULLSCREEN_COPY_OUT: &[OutputSocketContract] =
|
||||
&[output("color", Texture, OutputMetadata::None)];
|
||||
const LOCAL_IN: &[InputSocketContract] = &[input(
|
||||
"scene",
|
||||
TypeConstraint::Exact(SceneTable),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
)];
|
||||
const VISIBILITY_IN: &[InputSocketContract] = LOCAL_IN;
|
||||
const CULL_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"scene",
|
||||
TypeConstraint::Exact(SceneTable),
|
||||
"mesh",
|
||||
TypeConstraint::Exact(MeshData),
|
||||
REQUIRED,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
@@ -170,17 +160,11 @@ const CULL_IN: &[InputSocketContract] = &[
|
||||
REQUIRED,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
input(
|
||||
"frustum",
|
||||
TypeConstraint::Exact(CameraFrustum),
|
||||
REQUIRED,
|
||||
InputRole::UniformRead,
|
||||
),
|
||||
];
|
||||
const QUERY_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"scene",
|
||||
TypeConstraint::Exact(SceneTable),
|
||||
"mesh",
|
||||
TypeConstraint::Exact(MeshData),
|
||||
REQUIRED,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
@@ -197,10 +181,16 @@ const QUERY_IN: &[InputSocketContract] = &[
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
];
|
||||
const FORWARD_IN: &[InputSocketContract] = &[
|
||||
const REGISTRY_IN: &[InputSocketContract] = &[input(
|
||||
"pipelineIndices",
|
||||
TypeConstraint::Exact(PipelineIndexStream),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
)];
|
||||
const PIPELINE_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"scene",
|
||||
TypeConstraint::Exact(SceneTable),
|
||||
"mesh",
|
||||
TypeConstraint::Exact(MeshData),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
),
|
||||
@@ -210,24 +200,24 @@ const FORWARD_IN: &[InputSocketContract] = &[
|
||||
REQUIRED,
|
||||
InputRole::IndirectRead,
|
||||
),
|
||||
input(
|
||||
"activation",
|
||||
TypeConstraint::Exact(PipelineActivation),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
),
|
||||
input(
|
||||
"colorTarget",
|
||||
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
input(
|
||||
"depthTarget",
|
||||
TypeConstraint::OneOf(&[TextureSpec, Texture]),
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::DepthTarget,
|
||||
),
|
||||
input(
|
||||
"depthStencil",
|
||||
TypeConstraint::Exact(DepthStencilConfig),
|
||||
REQUIRED,
|
||||
InputRole::Configuration,
|
||||
),
|
||||
];
|
||||
const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
@@ -238,7 +228,7 @@ const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
|
||||
),
|
||||
input(
|
||||
"colorTarget",
|
||||
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
@@ -258,65 +248,33 @@ const BLOOM_COMPOSITE_IN: &[InputSocketContract] = &[
|
||||
),
|
||||
input(
|
||||
"colorTarget",
|
||||
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
];
|
||||
const PRESENT_IN: &[InputSocketContract] = &[input(
|
||||
"surface",
|
||||
const FRAME_OUT_IN: &[InputSocketContract] = &[input(
|
||||
"color",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::Present,
|
||||
InputRole::SampledTexture,
|
||||
)];
|
||||
|
||||
pub static CONTRACTS: &[Contract] = &[
|
||||
Contract {
|
||||
key: "surface_target",
|
||||
key: "mesh",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: SURFACE_OUT,
|
||||
outputs: MESH_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "texture_spec",
|
||||
key: "texture",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: SPEC_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "scene_table",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: SCENE_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "local_aabb_buffer",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: LOCAL_IN,
|
||||
outputs: AABB_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "camera_frustum",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: FRUSTUM_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "visibility_flags",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: VISIBILITY_IN,
|
||||
outputs: VISIBLE_OUT,
|
||||
outputs: TEXTURE_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
@@ -336,19 +294,19 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "depth_stencil_config",
|
||||
key: "pipeline_registry",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: CONFIG_OUT,
|
||||
execution: ExecutionClass::CpuPreparation,
|
||||
inputs: REGISTRY_IN,
|
||||
outputs: ACTIVATION_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "legacy_forward",
|
||||
key: "pipeline",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FORWARD_IN,
|
||||
outputs: FORWARD_OUT,
|
||||
inputs: PIPELINE_IN,
|
||||
outputs: PIPELINE_OUT,
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
@@ -400,10 +358,10 @@ pub static CONTRACTS: &[Contract] = &[
|
||||
inherently_observable: false,
|
||||
},
|
||||
Contract {
|
||||
key: "present",
|
||||
key: "frame_out",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Present,
|
||||
inputs: PRESENT_IN,
|
||||
execution: ExecutionClass::Frame,
|
||||
inputs: FRAME_OUT_IN,
|
||||
outputs: NONE_OUT,
|
||||
inherently_observable: true,
|
||||
},
|
||||
|
||||
@@ -49,4 +49,4 @@ impl GraphError {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub(crate) mod tests;
|
||||
|
||||
@@ -33,10 +33,7 @@ pub struct CompiledResource {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ResourcePlan {
|
||||
SurfaceTarget {
|
||||
family: u32,
|
||||
},
|
||||
TextureSpec {
|
||||
TextureSource {
|
||||
family: u32,
|
||||
residency: TextureResidency,
|
||||
descriptor: NormalizedTextureDescriptor,
|
||||
@@ -49,20 +46,22 @@ pub enum ResourcePlan {
|
||||
stored: bool,
|
||||
allocation: Option<AllocationRef>,
|
||||
},
|
||||
SceneTable,
|
||||
MeshData,
|
||||
LocalAabbBuffer {
|
||||
scene: u32,
|
||||
mesh: u32,
|
||||
},
|
||||
CameraFrustum,
|
||||
BooleanFlagBuffer {
|
||||
scene: u32,
|
||||
mesh: u32,
|
||||
flag: MeshFlag,
|
||||
},
|
||||
DrawStream {
|
||||
scene: u32,
|
||||
PipelineIndexStream {
|
||||
mesh: u32,
|
||||
},
|
||||
DepthStencilConfig {
|
||||
config: NormalizedDepthStencil,
|
||||
PipelineActivation {
|
||||
pipeline_indices: u32,
|
||||
},
|
||||
DrawStream {
|
||||
mesh: u32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -104,12 +103,12 @@ pub enum ExecutionKind {
|
||||
color_attachments: Vec<ColorAttachmentPlan>,
|
||||
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
||||
},
|
||||
Present {
|
||||
surface: u32,
|
||||
FrameOut {
|
||||
color: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ComputeWork {
|
||||
FrustumCull,
|
||||
@@ -184,29 +183,29 @@ pub enum AccessMode {
|
||||
store: StoreOp,
|
||||
full_overwrite: bool,
|
||||
},
|
||||
Present,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum NormalizedParameters {
|
||||
SurfaceTarget,
|
||||
TextureSpec {
|
||||
Texture {
|
||||
residency: TextureResidency,
|
||||
texture: NormalizedTextureDescriptor,
|
||||
descriptor: NormalizedTextureDescriptor,
|
||||
},
|
||||
Mesh,
|
||||
FrustumCull {
|
||||
camera: ActiveCamera,
|
||||
},
|
||||
SceneTable,
|
||||
LocalAabbBuffer,
|
||||
CameraFrustum,
|
||||
VisibilityFlags,
|
||||
FrustumCull,
|
||||
MeshQuery {
|
||||
filters: [NormalizedMeshFilter; 2],
|
||||
visible_predicate: RuntimePredicate,
|
||||
frustum_culled_predicate: RuntimePredicate,
|
||||
},
|
||||
DepthStencilConfig {
|
||||
config: NormalizedDepthStencil,
|
||||
},
|
||||
LegacyForward {
|
||||
PipelineRegistry,
|
||||
Pipeline {
|
||||
pipeline: String,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
clear_color: [f64; 4],
|
||||
},
|
||||
FullscreenCopy,
|
||||
@@ -227,22 +226,13 @@ pub enum NormalizedParameters {
|
||||
LuminanceEdge {
|
||||
strength: f32,
|
||||
},
|
||||
Present,
|
||||
FrameOut,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NormalizedMeshFilter {
|
||||
pub flag: MeshFlag,
|
||||
pub predicate: TriStatePredicate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NormalizedDepthStencil {
|
||||
pub depth_compare: CompareFunction,
|
||||
pub depth_write_enabled: bool,
|
||||
pub clear_depth: f32,
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ActiveCamera {
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
|
||||
@@ -288,9 +278,6 @@ pub struct TextureFamilyKey {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum TextureFamilySource {
|
||||
ImportedSurface {
|
||||
resource: u32,
|
||||
},
|
||||
AuthoredTexture {
|
||||
resource: u32,
|
||||
residency: TextureResidency,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -114,11 +114,13 @@ pub enum TriStatePredicate {
|
||||
RequiredFalse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MeshFilter {
|
||||
pub flag: MeshFlag,
|
||||
pub predicate: TriStatePredicate,
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RuntimePredicate {
|
||||
Any,
|
||||
RequiredTrue,
|
||||
RequiredFalse,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
+1967
-466
File diff suppressed because it is too large
Load Diff
@@ -42,10 +42,14 @@ fn mesh_query(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
if (i >= params.count) { return; }
|
||||
let draw_meta = metadata[i];
|
||||
var selected = true;
|
||||
if (params.visible_predicate != 0u) {
|
||||
if (params.visible_predicate == 3u) {
|
||||
selected = false;
|
||||
} else if (params.visible_predicate != 0u) {
|
||||
selected = matches(authored_visible[i], params.visible_predicate);
|
||||
}
|
||||
if (params.frustum_predicate != 0u) {
|
||||
if (selected && params.frustum_predicate == 3u) {
|
||||
selected = false;
|
||||
} else if (selected && params.frustum_predicate != 0u) {
|
||||
selected = selected && matches(frustum_flags[i], params.frustum_predicate);
|
||||
}
|
||||
commands[i] = Command(draw_meta.index_count, select(0u, 1u, selected), draw_meta.first_index, draw_meta.base_vertex, 0u);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mod legacy_forward;
|
||||
mod pipeline;
|
||||
|
||||
pub(super) use legacy_forward::{encode_compiled, encode_immediate};
|
||||
pub(super) use pipeline::{encode_compiled, encode_immediate};
|
||||
|
||||
+48
-48
@@ -56,25 +56,8 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
materials: &MaterialResources,
|
||||
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||
) -> Result<(), &'static str> {
|
||||
use crate::render_graph::{
|
||||
ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, ResourcePlan, StoreOp,
|
||||
};
|
||||
use crate::render_graph::{ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
|
||||
let view = |resource: u32| -> Result<&wgpu::TextureView, &'static str> {
|
||||
let is_surface = active
|
||||
.graph
|
||||
.resources
|
||||
.get(resource as usize)
|
||||
.is_some_and(|resource| {
|
||||
matches!(
|
||||
resource.plan,
|
||||
ResourcePlan::SurfaceTarget { family }
|
||||
| ResourcePlan::Texture { family, .. }
|
||||
if family == active.runtime.allocations.surface_family
|
||||
)
|
||||
});
|
||||
if is_surface {
|
||||
return Ok(surface);
|
||||
}
|
||||
let a = active
|
||||
.runtime
|
||||
.allocations
|
||||
@@ -93,15 +76,16 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
for (execution_index, prepared) in active.executions.iter().enumerate() {
|
||||
let profile_id = &active.graph.executions[execution_index].id;
|
||||
match prepared {
|
||||
PreparedExecution::PipelineRegistry => {}
|
||||
PreparedExecution::FrustumCull => {
|
||||
gpu.encode_frustum_cull(encoder, profile.as_deref_mut(), profile_id);
|
||||
}
|
||||
PreparedExecution::MeshQuery => {
|
||||
gpu.encode_mesh_query(encoder, profile.as_deref_mut(), profile_id);
|
||||
}
|
||||
PreparedExecution::Present => {}
|
||||
PreparedExecution::Fullscreen {
|
||||
execution,
|
||||
frame_out,
|
||||
bind_group,
|
||||
pipeline,
|
||||
..
|
||||
@@ -111,22 +95,30 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
.executions
|
||||
.get(*execution)
|
||||
.ok_or(" execution out of bounds")?;
|
||||
let ExecutionKind::Render {
|
||||
color_attachments, ..
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err("fullscreen is not render");
|
||||
};
|
||||
let color = color_attachments
|
||||
.first()
|
||||
.ok_or("fullscreen target missing")?;
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&execution.id),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: view(color.resource)?,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
let (target, operations) = if *frame_out {
|
||||
let ExecutionKind::FrameOut { .. } = execution.kind else {
|
||||
return Err("frame_out kind mismatch");
|
||||
};
|
||||
(
|
||||
surface,
|
||||
wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments, ..
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err("fullscreen is not render");
|
||||
};
|
||||
let color = color_attachments
|
||||
.first()
|
||||
.ok_or("fullscreen target missing")?;
|
||||
(
|
||||
view(color.resource)?,
|
||||
wgpu::Operations {
|
||||
load: match color.load {
|
||||
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
|
||||
NormalizedColorLoad::Clear { value } => {
|
||||
@@ -144,6 +136,15 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
wgpu::StoreOp::Discard
|
||||
},
|
||||
},
|
||||
)
|
||||
};
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&execution.id),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: operations,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
@@ -155,9 +156,10 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
pass.set_bind_group(0, bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
PreparedExecution::LegacyForward {
|
||||
PreparedExecution::Pipeline {
|
||||
execution,
|
||||
variants,
|
||||
base,
|
||||
variant,
|
||||
} => {
|
||||
let execution = active
|
||||
.graph
|
||||
@@ -169,10 +171,10 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
depth_stencil,
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err("legacy forward is not render");
|
||||
return Err("pipeline is not render");
|
||||
};
|
||||
let color = color_attachments.first().ok_or("legacy color missing")?;
|
||||
let depth = depth_stencil.as_ref().ok_or("legacy depth missing")?;
|
||||
let color = color_attachments.first().ok_or("pipeline color missing")?;
|
||||
let depth = depth_stencil.as_ref().ok_or("pipeline depth missing")?;
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(&execution.id),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
@@ -235,16 +237,14 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
pass.set_vertex_buffer(4, t.slice(..));
|
||||
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for draw in &gpu.draws {
|
||||
if draw.pipeline != *base {
|
||||
continue;
|
||||
}
|
||||
let slot = draw.instances.start as u64;
|
||||
let start = slot
|
||||
* std::mem::size_of::<crate::renderer::gpu_scene::GpuInstance>() as u64;
|
||||
pass.set_vertex_buffer(3, inst.slice(start..start + 112));
|
||||
let key = variants
|
||||
.iter()
|
||||
.find(|(base, _)| *base == draw.pipeline)
|
||||
.map(|x| &x.1)
|
||||
.ok_or("pipeline variant missing")?;
|
||||
pass.set_pipeline(key);
|
||||
pass.set_pipeline(variant);
|
||||
if pipelines.requires_material(draw.pipeline) {
|
||||
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
||||
}
|
||||
@@ -274,7 +274,7 @@ pub(crate) fn encode_immediate<T: Scene>(
|
||||
profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||
) {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Render pass"),
|
||||
label: Some("Immediate pipeline pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
depth_slice: None,
|
||||
view: color,
|
||||
@@ -298,7 +298,7 @@ pub(crate) fn encode_immediate<T: Scene>(
|
||||
stencil_ops: None,
|
||||
}),
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: profile.and_then(|p| p.render_writes("immediate.forward")),
|
||||
timestamp_writes: profile.and_then(|p| p.render_writes("immediate.pipeline")),
|
||||
});
|
||||
encode_scene(&mut pass, scene, gpu, pipelines, materials);
|
||||
}
|
||||
@@ -85,8 +85,8 @@ impl GpuScenePlan {
|
||||
self::GpuScenePlan::build_with_query(
|
||||
data,
|
||||
crate::render_graph::MeshQueryRuntimeKey {
|
||||
visible: crate::render_graph::TriStatePredicate::RequiredTrue,
|
||||
frustum_culled: crate::render_graph::TriStatePredicate::Any,
|
||||
visible: crate::render_graph::RuntimePredicate::RequiredTrue,
|
||||
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -277,7 +277,6 @@ pub struct BufferSlot {
|
||||
#[derive(Default)]
|
||||
pub struct GpuSceneCache {
|
||||
revision: Option<u64>,
|
||||
query: Option<crate::render_graph::MeshQueryRuntimeKey>,
|
||||
pub positions: BufferSlot,
|
||||
pub normals: BufferSlot,
|
||||
pub uvs: BufferSlot,
|
||||
@@ -310,11 +309,12 @@ struct CullingParams {
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
fn predicate_code(value: crate::render_graph::TriStatePredicate) -> u32 {
|
||||
fn predicate_code(value: crate::render_graph::RuntimePredicate) -> u32 {
|
||||
match value {
|
||||
crate::render_graph::TriStatePredicate::Any => 0,
|
||||
crate::render_graph::TriStatePredicate::RequiredTrue => 1,
|
||||
crate::render_graph::TriStatePredicate::RequiredFalse => 2,
|
||||
crate::render_graph::RuntimePredicate::Any => 0,
|
||||
crate::render_graph::RuntimePredicate::RequiredTrue => 1,
|
||||
crate::render_graph::RuntimePredicate::RequiredFalse => 2,
|
||||
crate::render_graph::RuntimePredicate::Never => 3,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,8 +330,8 @@ impl GpuSceneCache {
|
||||
queue,
|
||||
data,
|
||||
crate::render_graph::MeshQueryRuntimeKey {
|
||||
visible: crate::render_graph::TriStatePredicate::RequiredTrue,
|
||||
frustum_culled: crate::render_graph::TriStatePredicate::Any,
|
||||
visible: crate::render_graph::RuntimePredicate::RequiredTrue,
|
||||
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -343,14 +343,13 @@ impl GpuSceneCache {
|
||||
data: &SceneFramePlan,
|
||||
query: crate::render_graph::MeshQueryRuntimeKey,
|
||||
) -> Result<(), String> {
|
||||
if self.revision == Some(data.revision) && self.query == Some(query) {
|
||||
if self.revision == Some(data.revision) {
|
||||
return Ok(());
|
||||
}
|
||||
let plan = GpuScenePlan::build_with_query(data, query).map_err(str::to_owned)?;
|
||||
if plan.draws.is_empty() {
|
||||
self.draws.clear();
|
||||
self.revision = Some(data.revision);
|
||||
self.query = Some(query);
|
||||
return Ok(());
|
||||
}
|
||||
let maximum = device.limits().max_buffer_size;
|
||||
@@ -485,7 +484,6 @@ impl GpuSceneCache {
|
||||
self.draws = plan.draws;
|
||||
self.rebuild_compute(device)?;
|
||||
self.revision = Some(data.revision);
|
||||
self.query = Some(query);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -642,11 +640,28 @@ mod tests {
|
||||
#[test]
|
||||
fn mesh_query_source_guards_optional_flag_buffer_reads() {
|
||||
let source = include_str!("culling.wgsl");
|
||||
let visible_guard = source.find("if (params.visible_predicate != 0u)").unwrap();
|
||||
let visible_never = source.find("if (params.visible_predicate == 3u)").unwrap();
|
||||
let visible_guard = source
|
||||
.find("else if (params.visible_predicate != 0u)")
|
||||
.unwrap();
|
||||
let visible_load = source.find("matches(authored_visible[i]").unwrap();
|
||||
let frustum_guard = source.find("if (params.frustum_predicate != 0u)").unwrap();
|
||||
let frustum_never = source
|
||||
.find("if (selected && params.frustum_predicate == 3u)")
|
||||
.unwrap();
|
||||
let frustum_guard = source
|
||||
.find("else if (selected && params.frustum_predicate != 0u)")
|
||||
.unwrap();
|
||||
let frustum_load = source.find("matches(frustum_flags[i]").unwrap();
|
||||
assert!(visible_guard < visible_load && frustum_guard < frustum_load);
|
||||
assert!(visible_never < visible_guard && visible_guard < visible_load);
|
||||
assert!(frustum_never < frustum_guard && frustum_guard < frustum_load);
|
||||
assert!(
|
||||
visible_load < frustum_load,
|
||||
"visible rejection must precede the frustum load"
|
||||
);
|
||||
assert!(
|
||||
source.contains("predicate == 0u ||"),
|
||||
"predicate zero is handled without a load by the guards"
|
||||
);
|
||||
for binding in 0..=6 {
|
||||
assert!(source.contains(&format!("@binding({binding})")));
|
||||
}
|
||||
|
||||
+155
-103
@@ -35,17 +35,19 @@ struct GpuTextureSlot {
|
||||
enum PreparedExecution {
|
||||
FrustumCull,
|
||||
MeshQuery,
|
||||
LegacyForward {
|
||||
PipelineRegistry,
|
||||
Pipeline {
|
||||
execution: usize,
|
||||
variants: Vec<(crate::render_data::PipelineKey, wgpu::RenderPipeline)>,
|
||||
base: crate::render_data::PipelineKey,
|
||||
variant: wgpu::RenderPipeline,
|
||||
},
|
||||
Fullscreen {
|
||||
execution: usize,
|
||||
frame_out: bool,
|
||||
bind_group: wgpu::BindGroup,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
_uniform: wgpu::Buffer,
|
||||
},
|
||||
Present,
|
||||
}
|
||||
|
||||
struct ActiveCompiledGraph {
|
||||
@@ -81,7 +83,10 @@ fn resolve_culling_frustum(
|
||||
query: crate::render_graph::MeshQueryRuntimeKey,
|
||||
read: impl FnOnce() -> Option<Result<[[f32; 4]; 6], crate::camera::FrustumError>>,
|
||||
) -> Result<Option<[[f32; 4]; 6]>, crate::render_graph::GraphError> {
|
||||
if query.frustum_culled == crate::render_graph::TriStatePredicate::Any {
|
||||
if matches!(
|
||||
query.frustum_culled,
|
||||
crate::render_graph::RuntimePredicate::Any | crate::render_graph::RuntimePredicate::Never
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
match read() {
|
||||
@@ -177,18 +182,25 @@ fn resolve_switch_request(
|
||||
|
||||
#[cfg(test)]
|
||||
mod switch_request_tests {
|
||||
fn valid_compile_graph(graph_id: &str, revision: u64) -> Vec<u8> {
|
||||
let mut graph = crate::render_graph::tests::full_cull_graph();
|
||||
graph["graphId"] = serde_json::json!(graph_id);
|
||||
graph["revision"] = serde_json::json!(revision);
|
||||
serde_json::to_vec(&graph).unwrap()
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
fn query(visible: crate::render_graph::TriStatePredicate) -> UploadGraph {
|
||||
fn query(visible: crate::render_graph::RuntimePredicate) -> UploadGraph {
|
||||
UploadGraph::Compiled(crate::render_graph::MeshQueryRuntimeKey {
|
||||
visible,
|
||||
frustum_culled: crate::render_graph::TriStatePredicate::Any,
|
||||
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_selection_follows_the_graph_rendered_for_the_commit_frame() {
|
||||
use crate::render_graph::TriStatePredicate::{Any, RequiredFalse, RequiredTrue};
|
||||
use crate::render_graph::RuntimePredicate::{Any, RequiredFalse, RequiredTrue};
|
||||
let selected =
|
||||
|pending, active| upload_query_for_render(pending, active).map(|query| query.visible);
|
||||
assert_eq!(
|
||||
@@ -214,7 +226,7 @@ mod switch_request_tests {
|
||||
|
||||
#[test]
|
||||
fn frustum_preflight_skips_any_and_distinguishes_missing_from_invalid() {
|
||||
use crate::render_graph::TriStatePredicate::{Any, RequiredFalse, RequiredTrue};
|
||||
use crate::render_graph::RuntimePredicate::{Any, RequiredFalse, RequiredTrue};
|
||||
let query = |frustum_culled| crate::render_graph::MeshQueryRuntimeKey {
|
||||
visible: RequiredTrue,
|
||||
frustum_culled,
|
||||
@@ -245,8 +257,10 @@ mod switch_request_tests {
|
||||
#[test]
|
||||
fn resolves_at_command_boundary_before_gpu_work() {
|
||||
let mut registry = crate::render_graph::Registry::default();
|
||||
let bytes = br#"{"schemaVersion":2,"graphId":"switch","revision":1,"nodes":[]}"#;
|
||||
let (id, _) = registry.compile(bytes).unwrap();
|
||||
let mut graph = crate::render_graph::tests::full_cull_graph();
|
||||
graph["graphId"] = serde_json::json!("switch");
|
||||
let bytes = serde_json::to_vec(&graph).unwrap();
|
||||
let (id, _) = registry.compile(&bytes).unwrap();
|
||||
let active = "existing_graph";
|
||||
let pending: Option<&str> = None;
|
||||
assert_eq!(
|
||||
@@ -279,9 +293,7 @@ mod switch_request_tests {
|
||||
#[test]
|
||||
fn resize_restart_snapshot_remains_bound_to_its_immutable_registry_revision() {
|
||||
let mut registry = crate::render_graph::Registry::default();
|
||||
let (id, _) = registry
|
||||
.compile(br#"{"schemaVersion":2,"graphId":"resize","revision":1,"nodes":[]}"#)
|
||||
.unwrap();
|
||||
let (id, _) = registry.compile(&valid_compile_graph("resize", 1)).unwrap();
|
||||
let revision_one = registry.get(id).unwrap().clone();
|
||||
let in_flight = InFlightPreparation {
|
||||
token: 1,
|
||||
@@ -289,9 +301,7 @@ mod switch_request_tests {
|
||||
purpose: PreparationPurpose::Resize,
|
||||
graph: revision_one,
|
||||
};
|
||||
let (revision_two_id, _) = registry
|
||||
.compile(br#"{"schemaVersion":2,"graphId":"resize","revision":2,"nodes":[]}"#)
|
||||
.unwrap();
|
||||
let (revision_two_id, _) = registry.compile(&valid_compile_graph("resize", 2)).unwrap();
|
||||
let original = registry.get(id).unwrap();
|
||||
let revision_two = registry.get(revision_two_id).unwrap();
|
||||
assert_eq!(in_flight.graph.revision, 1);
|
||||
@@ -728,6 +738,26 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
) -> Result<ActiveCompiledGraph, crate::render_graph::GraphError> {
|
||||
use crate::render_graph::*;
|
||||
let fail = |message| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", message);
|
||||
let resolved_pipelines = graph
|
||||
.executions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, execution)| {
|
||||
let NormalizedParameters::Pipeline { pipeline, .. } = &execution.parameters else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.resources
|
||||
.find_pipeline(pipeline)
|
||||
.map(Some)
|
||||
.ok_or_else(|| {
|
||||
GraphError::at(
|
||||
"GRAPH_EXECUTION_UNSUPPORTED",
|
||||
format!("pipeline '{pipeline}' is not registered"),
|
||||
format!("executions[{index}].parameters.pipeline"),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut textures = Vec::with_capacity(runtime.allocations.classes.len());
|
||||
for class in &runtime.allocations.classes {
|
||||
let mut gpu_class = Vec::with_capacity(class.slots.len());
|
||||
@@ -845,37 +875,53 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
match execution.executor.key.as_str() {
|
||||
"frustum_cull" => executions.push(PreparedExecution::FrustumCull),
|
||||
"mesh_query" => executions.push(PreparedExecution::MeshQuery),
|
||||
"present" => executions.push(PreparedExecution::Present),
|
||||
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
|
||||
"frame_out" | "fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
|
||||
| "bloom_composite" | "luminance_edge" => {
|
||||
let sampled: Vec<_> = execution
|
||||
.accesses
|
||||
.iter()
|
||||
.filter(|a| matches!(a.mode, AccessMode::SampledTexture))
|
||||
.map(|a| a.resource)
|
||||
.collect();
|
||||
let source = *sampled
|
||||
.first()
|
||||
.ok_or_else(|| fail("fullscreen source missing"))?;
|
||||
let second = *sampled.get(1).unwrap_or(&source);
|
||||
let values: [f32; 8] = match execution.parameters {
|
||||
NormalizedParameters::ToneMap { exposure } => {
|
||||
[exposure, 0., 0., 0., 0., 0., 0., 0.]
|
||||
let frame_out = execution.executor.key == "frame_out";
|
||||
let (source, second) = if frame_out {
|
||||
let ExecutionKind::FrameOut { color } = execution.kind else {
|
||||
return Err(fail("frame_out kind mismatch"));
|
||||
};
|
||||
(color, color)
|
||||
} else {
|
||||
match execution.inputs.as_slice() {
|
||||
[source, _color_target] => (source.resource, source.resource),
|
||||
[source, bloom, _color_target]
|
||||
if execution.executor.key == "bloom_composite" =>
|
||||
{
|
||||
(source.resource, bloom.resource)
|
||||
}
|
||||
_ => return Err(fail("fullscreen inputs mismatch")),
|
||||
}
|
||||
NormalizedParameters::BloomExtract { threshold, knee } => {
|
||||
[threshold, knee, 0., 0., 0., 0., 0., 0.]
|
||||
}
|
||||
NormalizedParameters::BloomBlur { direction, radius } => {
|
||||
[direction[0], direction[1], radius, 0., 0., 0., 0., 0.]
|
||||
}
|
||||
NormalizedParameters::BloomComposite { intensity } => {
|
||||
[intensity, 0., 0., 0., 0., 0., 0., 0.]
|
||||
}
|
||||
NormalizedParameters::LuminanceEdge { strength } => {
|
||||
[strength, 0., 0., 0., 0., 0., 0., 0.]
|
||||
}
|
||||
_ => [0.; 8],
|
||||
};
|
||||
let values: [f32; 8] =
|
||||
match (execution.executor.key.as_str(), &execution.parameters) {
|
||||
(
|
||||
"fullscreen_copy" | "frame_out",
|
||||
NormalizedParameters::FullscreenCopy
|
||||
| NormalizedParameters::FrameOut,
|
||||
) => [0.; 8],
|
||||
("tone_map", NormalizedParameters::ToneMap { exposure }) => {
|
||||
[*exposure, 0., 0., 0., 0., 0., 0., 0.]
|
||||
}
|
||||
(
|
||||
"bloom_extract",
|
||||
NormalizedParameters::BloomExtract { threshold, knee },
|
||||
) => [*threshold, *knee, 0., 0., 0., 0., 0., 0.],
|
||||
(
|
||||
"bloom_blur",
|
||||
NormalizedParameters::BloomBlur { direction, radius },
|
||||
) => [direction[0], direction[1], *radius, 0., 0., 0., 0., 0.],
|
||||
(
|
||||
"bloom_composite",
|
||||
NormalizedParameters::BloomComposite { intensity },
|
||||
) => [*intensity, 0., 0., 0., 0., 0., 0., 0.],
|
||||
(
|
||||
"luminance_edge",
|
||||
NormalizedParameters::LuminanceEdge { strength },
|
||||
) => [*strength, 0., 0., 0., 0., 0., 0., 0.],
|
||||
_ => return Err(fail("executor parameters mismatch")),
|
||||
};
|
||||
use wgpu::util::DeviceExt;
|
||||
let uniform =
|
||||
self.context
|
||||
@@ -885,17 +931,35 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
contents: bytemuck::cast_slice(&values),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let ExecutionKind::Render {
|
||||
color_attachments, ..
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err(fail("fullscreen execution is not render"));
|
||||
let target_format = if frame_out {
|
||||
runtime.surface.format
|
||||
} else {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments, ..
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err(fail("fullscreen execution is not render"));
|
||||
};
|
||||
let target = color_attachments
|
||||
.first()
|
||||
.ok_or_else(|| fail("fullscreen target missing"))?
|
||||
.resource;
|
||||
let a = runtime
|
||||
.allocations
|
||||
.resource_allocations
|
||||
.get(target as usize)
|
||||
.copied()
|
||||
.flatten()
|
||||
.ok_or_else(|| fail("fullscreen target allocation missing"))?;
|
||||
runtime
|
||||
.allocations
|
||||
.classes
|
||||
.get(a.class as usize)
|
||||
.and_then(|class| class.slots.get(a.slot as usize))
|
||||
.ok_or_else(|| fail("fullscreen target allocation is invalid"))?
|
||||
.descriptor
|
||||
.format
|
||||
};
|
||||
let target = color_attachments
|
||||
.first()
|
||||
.ok_or_else(|| fail("fullscreen target missing"))?
|
||||
.resource;
|
||||
let target_format = if graph.resources.get(target as usize).is_some_and(|r| matches!(r.plan, ResourcePlan::Texture { family, .. } if family == runtime.allocations.surface_family)) { runtime.surface.format } else { let a=runtime.allocations.resource_allocations[target as usize].ok_or_else(|| fail("fullscreen target allocation missing"))?; runtime.allocations.classes[a.class as usize].slots[a.slot as usize].descriptor.format };
|
||||
let entry = match execution.executor.key.as_str() {
|
||||
"fullscreen_copy" => "fs_copy",
|
||||
"tone_map" => "fs_tone_map",
|
||||
@@ -903,7 +967,8 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
"bloom_blur" => "fs_bloom_blur",
|
||||
"bloom_composite" => "fs_bloom_composite",
|
||||
"luminance_edge" => "fs_luminance_edge",
|
||||
_ => unreachable!(),
|
||||
"frame_out" => "fs_copy",
|
||||
_ => return Err(fail("fullscreen executor mismatch")),
|
||||
};
|
||||
let pipeline = self.context.device.create_render_pipeline(
|
||||
&wgpu::RenderPipelineDescriptor {
|
||||
@@ -963,36 +1028,25 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
});
|
||||
executions.push(PreparedExecution::Fullscreen {
|
||||
execution: index,
|
||||
frame_out,
|
||||
bind_group,
|
||||
pipeline,
|
||||
_uniform: uniform,
|
||||
});
|
||||
}
|
||||
"legacy_forward" => {
|
||||
"pipeline_registry" => executions.push(PreparedExecution::PipelineRegistry),
|
||||
"pipeline" => {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
depth_stencil,
|
||||
} = &execution.kind
|
||||
else {
|
||||
return Err(fail("legacy forward is not render"));
|
||||
return Err(fail("pipeline is not render"));
|
||||
};
|
||||
let color = color_attachments
|
||||
.first()
|
||||
.ok_or_else(|| fail("legacy color missing"))?;
|
||||
let color_is_surface = graph
|
||||
.resources
|
||||
.get(color.resource as usize)
|
||||
.is_some_and(|resource| {
|
||||
matches!(
|
||||
resource.plan,
|
||||
ResourcePlan::SurfaceTarget { family }
|
||||
| ResourcePlan::Texture { family, .. }
|
||||
if family == runtime.allocations.surface_family
|
||||
)
|
||||
});
|
||||
let color_format = if color_is_surface {
|
||||
runtime.surface.format
|
||||
} else {
|
||||
.ok_or_else(|| fail("pipeline color missing"))?;
|
||||
let color_format = {
|
||||
let a = runtime
|
||||
.allocations
|
||||
.resource_allocations
|
||||
@@ -1027,19 +1081,21 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.ok_or_else(|| fail("depth allocation invalid"))
|
||||
})
|
||||
.transpose()?;
|
||||
let config = execution
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|i| graph.resources.get(i.resource as usize))
|
||||
.find_map(|r| {
|
||||
if let ResourcePlan::DepthStencilConfig { config } = r.plan {
|
||||
Some(config)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| fail("depth config missing"))?;
|
||||
let compare = match config.depth_compare {
|
||||
let NormalizedParameters::Pipeline {
|
||||
pipeline: _,
|
||||
depth_compare,
|
||||
depth_write_enabled,
|
||||
..
|
||||
} = &execution.parameters
|
||||
else {
|
||||
return Err(fail("pipeline parameters mismatch"));
|
||||
};
|
||||
let base = resolved_pipelines
|
||||
.get(index)
|
||||
.copied()
|
||||
.flatten()
|
||||
.ok_or_else(|| fail("resolved pipeline missing"))?;
|
||||
let compare = match depth_compare {
|
||||
CompareFunction::Never => wgpu::CompareFunction::Never,
|
||||
CompareFunction::Less => wgpu::CompareFunction::Less,
|
||||
CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual,
|
||||
@@ -1049,25 +1105,21 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual,
|
||||
CompareFunction::Always => wgpu::CompareFunction::Always,
|
||||
};
|
||||
let mut variants = Vec::new();
|
||||
let bases: Vec<_> = self.resources.pipeline_keys().collect();
|
||||
for base in bases {
|
||||
let variant = self
|
||||
.resources
|
||||
.create_target_variant(
|
||||
&self.context.device,
|
||||
base,
|
||||
color_format,
|
||||
depth_format,
|
||||
compare,
|
||||
config.depth_write_enabled,
|
||||
)
|
||||
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
||||
variants.push((base, variant));
|
||||
}
|
||||
executions.push(PreparedExecution::LegacyForward {
|
||||
let variant = self
|
||||
.resources
|
||||
.create_target_variant(
|
||||
&self.context.device,
|
||||
base,
|
||||
color_format,
|
||||
depth_format,
|
||||
compare,
|
||||
*depth_write_enabled,
|
||||
)
|
||||
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
||||
executions.push(PreparedExecution::Pipeline {
|
||||
execution: index,
|
||||
variants,
|
||||
base,
|
||||
variant,
|
||||
});
|
||||
}
|
||||
_ => return Err(fail("unsupported prepared execution")),
|
||||
|
||||
@@ -372,36 +372,6 @@ impl PipelineLibrary {
|
||||
&& self.specs[key.get() as usize].layout == self.material_layout
|
||||
}
|
||||
|
||||
pub fn pipeline_keys(&self) -> impl Iterator<Item = PipelineKey> + '_ {
|
||||
let mut keys = self
|
||||
.pipeline_registry
|
||||
.values()
|
||||
.map(|entry| entry.0)
|
||||
.collect::<Vec<_>>();
|
||||
keys.sort_by_key(|key| key.get());
|
||||
keys.dedup();
|
||||
keys.into_iter()
|
||||
}
|
||||
|
||||
pub fn get_or_create_target_variant(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
base: PipelineKey,
|
||||
color_format: wgpu::TextureFormat,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_compare: wgpu::CompareFunction,
|
||||
depth_write: bool,
|
||||
) -> Result<PipelineKey, String> {
|
||||
let spec = self
|
||||
.specs
|
||||
.get(base.get() as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| "unknown base pipeline".to_owned())?;
|
||||
let spec =
|
||||
target_variant_spec(spec, color_format, depth_format, depth_compare, depth_write);
|
||||
Ok(self.get_or_create_from_spec(device, &spec, Some("target variant")))
|
||||
}
|
||||
|
||||
pub fn create_target_variant(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
|
||||
+267
-41
@@ -56,14 +56,20 @@ const sourceMaps = new WeakMap();
|
||||
export const getSourceMap = (ir) => sourceMaps.get(ir);
|
||||
export const mapAuthoringDiagnostic = (ir, diagnostic) => {
|
||||
const details = diagnostic?.details;
|
||||
const path = [details?.path, diagnostic?.path, details?.field, diagnostic?.field]
|
||||
.find((value) => typeof value === "string");
|
||||
const path = [
|
||||
details?.path,
|
||||
diagnostic?.path,
|
||||
details?.field,
|
||||
diagnostic?.field,
|
||||
].find((value) => typeof value === "string");
|
||||
const map = getSourceMap(ir);
|
||||
let match;
|
||||
if (path && map)
|
||||
for (const key of Object.keys(map))
|
||||
if (
|
||||
(path === key || path.startsWith(`${key}.`) || path.startsWith(`${key}[`)) &&
|
||||
(path === key ||
|
||||
path.startsWith(`${key}.`) ||
|
||||
path.startsWith(`${key}[`)) &&
|
||||
(!match || key.length > match.length)
|
||||
)
|
||||
match = key;
|
||||
@@ -80,33 +86,49 @@ export const mapAuthoringDiagnostic = (ir, diagnostic) => {
|
||||
const mapValuePaths = (paths, path, source, value) => {
|
||||
paths[path] = source;
|
||||
if (Array.isArray(value))
|
||||
value.forEach((child, index) => mapValuePaths(paths, `${path}[${index}]`, source, child));
|
||||
value.forEach((child, index) =>
|
||||
mapValuePaths(paths, `${path}[${index}]`, source, child),
|
||||
);
|
||||
else if (object(value))
|
||||
for (const key of Object.keys(value))
|
||||
mapValuePaths(paths, `${path}.${key}`, source, value[key]);
|
||||
};
|
||||
|
||||
function parameterValue(raw, schema, nodeId, key) {
|
||||
const expected = schema.type === "json" ? "json" : schema.type;
|
||||
if (
|
||||
!exactKeys(raw, ["kind", "value"]) ||
|
||||
raw.kind !== expected ||
|
||||
!finiteJson(raw.value)
|
||||
)
|
||||
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
if (
|
||||
(expected === "number" && typeof raw.value !== "number") ||
|
||||
(expected === "string" && typeof raw.value !== "string") ||
|
||||
(expected === "boolean" && typeof raw.value !== "boolean") ||
|
||||
(expected === "json" && !finiteJson(raw.value))
|
||||
)
|
||||
if (!exactKeys(raw, ["kind", "value"]) || raw.kind !== schema.type)
|
||||
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
const value = raw.value;
|
||||
const bounded = (number) =>
|
||||
Number.isFinite(number) &&
|
||||
(schema.minimum === undefined || number >= schema.minimum) &&
|
||||
(schema.maximum === undefined || number <= schema.maximum);
|
||||
const valid =
|
||||
schema.type === "number"
|
||||
? bounded(value) && (!schema.integer || Number.isSafeInteger(value))
|
||||
: schema.type === "string"
|
||||
? typeof value === "string" &&
|
||||
(!schema.enum || schema.enum.includes(value))
|
||||
: schema.type === "boolean"
|
||||
? typeof value === "boolean"
|
||||
: schema.type === "vector" || schema.type === "color"
|
||||
? Array.isArray(value) &&
|
||||
value.length === (schema.type === "vector" ? 3 : 4) &&
|
||||
value.every(bounded)
|
||||
: schema.type === "json" && finiteJson(value);
|
||||
if (!valid) fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
return canonical(structuredClone(raw.value));
|
||||
}
|
||||
|
||||
export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
try {
|
||||
const rootKeys = ["graphId", "catalogVersion", "nodes", "links", "metadata", "version"];
|
||||
const rootKeys = [
|
||||
"graphId",
|
||||
"catalogVersion",
|
||||
"nodes",
|
||||
"links",
|
||||
"metadata",
|
||||
"version",
|
||||
];
|
||||
if (
|
||||
!exactKeys(raw, rootKeys) ||
|
||||
!Array.isArray(raw.nodes) ||
|
||||
@@ -117,9 +139,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
fail("AUTHORING_SHAPE");
|
||||
if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION)
|
||||
fail("AUTHORING_CATALOG");
|
||||
if (
|
||||
!Number.isSafeInteger(raw.version) || raw.version < 0
|
||||
)
|
||||
if (!Number.isSafeInteger(raw.version) || raw.version < 0)
|
||||
fail("AUTHORING_SHAPE");
|
||||
if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff)
|
||||
fail("AUTHORING_REVISION");
|
||||
@@ -134,7 +154,20 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
definition = nodeDefinitions[n.typeId];
|
||||
if (!descriptor)
|
||||
fail("AUTHORING_NODE_TYPE", { nodeId: n.id, typeId: n.typeId });
|
||||
const nodeKeys = ["id", "typeId", "typeVersion", "position", "size", "label", "parameters", "sockets", "muted", "collapsed", "extensions", "known"];
|
||||
const nodeKeys = [
|
||||
"id",
|
||||
"typeId",
|
||||
"typeVersion",
|
||||
"position",
|
||||
"size",
|
||||
"label",
|
||||
"parameters",
|
||||
"sockets",
|
||||
"muted",
|
||||
"collapsed",
|
||||
"extensions",
|
||||
"known",
|
||||
];
|
||||
if (Object.hasOwn(n, "parentId")) nodeKeys.push("parentId");
|
||||
if (
|
||||
!exactKeys(n, nodeKeys) ||
|
||||
@@ -143,10 +176,17 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
typeof n.muted !== "boolean" ||
|
||||
typeof n.collapsed !== "boolean" ||
|
||||
typeof n.label !== "string" ||
|
||||
!exactKeys(n.position, ["x", "y"]) || !Number.isFinite(n.position.x) || !Number.isFinite(n.position.y) ||
|
||||
!exactKeys(n.size, ["x", "y"]) || !Number.isFinite(n.size.x) || !Number.isFinite(n.size.y) || n.size.x <= 0 || n.size.y <= 0 ||
|
||||
!exactKeys(n.position, ["x", "y"]) ||
|
||||
!Number.isFinite(n.position.x) ||
|
||||
!Number.isFinite(n.position.y) ||
|
||||
!exactKeys(n.size, ["x", "y"]) ||
|
||||
!Number.isFinite(n.size.x) ||
|
||||
!Number.isFinite(n.size.y) ||
|
||||
n.size.x <= 0 ||
|
||||
n.size.y <= 0 ||
|
||||
(Object.hasOwn(n, "parentId") && !identifier(n.parentId)) ||
|
||||
!object(n.extensions) || !finiteJson(n.extensions) ||
|
||||
!object(n.extensions) ||
|
||||
!finiteJson(n.extensions) ||
|
||||
!Array.isArray(n.sockets) ||
|
||||
!object(n.parameters)
|
||||
)
|
||||
@@ -168,6 +208,48 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
),
|
||||
]),
|
||||
);
|
||||
if (n.typeId === "bloom_blur")
|
||||
parameters.direction =
|
||||
parameters.direction === "horizontal" ? [1, 0] : [0, 1];
|
||||
if (n.typeId === "frustum_cull") {
|
||||
parameters.camera = parameters.cameraSelection;
|
||||
delete parameters.cameraSelection;
|
||||
}
|
||||
if (n.typeId === "texture") {
|
||||
const extent =
|
||||
parameters.extentMode === "absolute"
|
||||
? {
|
||||
kind: "absolute",
|
||||
width: parameters.absoluteWidth,
|
||||
height: parameters.absoluteHeight,
|
||||
depthOrArrayLayers: parameters.depthOrArrayLayers,
|
||||
}
|
||||
: {
|
||||
kind: "surface_relative",
|
||||
width: {
|
||||
numerator: parameters.relativeWidthNumerator,
|
||||
denominator: parameters.relativeWidthDenominator,
|
||||
},
|
||||
height: {
|
||||
numerator: parameters.relativeHeightNumerator,
|
||||
denominator: parameters.relativeHeightDenominator,
|
||||
},
|
||||
depthOrArrayLayers: parameters.depthOrArrayLayers,
|
||||
};
|
||||
const flat = structuredClone(parameters);
|
||||
Object.keys(parameters).forEach((key) => delete parameters[key]);
|
||||
Object.assign(parameters, {
|
||||
residency: flat.residency,
|
||||
texture: {
|
||||
dimension: flat.dimension,
|
||||
format: flat.format,
|
||||
extent,
|
||||
mipLevelCount: flat.mipLevelCount,
|
||||
sampleCount: Number(flat.sampleCount),
|
||||
viewFormats: flat.viewFormat === "none" ? [] : [flat.viewFormat],
|
||||
},
|
||||
});
|
||||
}
|
||||
const expected = [
|
||||
...Object.keys(descriptor.inputs),
|
||||
...Object.keys(descriptor.outputs),
|
||||
@@ -181,17 +263,49 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
socketDefinition = definition.sockets[s.key],
|
||||
direction = input ? "input" : "output",
|
||||
dataType = socketDefinition.type,
|
||||
socketKeys = ["id", "key", "label", "direction", "dataType", "accepts", "maxIncomingLinks", ...(socketDefinition.value ? ["defaultValue"] : []), "visible"];
|
||||
socketKeys = [
|
||||
"id",
|
||||
"key",
|
||||
"label",
|
||||
"direction",
|
||||
"dataType",
|
||||
"accepts",
|
||||
"maxIncomingLinks",
|
||||
...(socketDefinition.value ? ["defaultValue"] : []),
|
||||
"visible",
|
||||
];
|
||||
if (
|
||||
!exactKeys(s, socketKeys) ||
|
||||
s.id !== `${n.id}:${s.key}` ||
|
||||
s.label !== socketDefinition.title ||
|
||||
s.direction !== direction ||
|
||||
s.dataType !== dataType ||
|
||||
!Array.isArray(s.accepts) || s.accepts.length !== (direction === "input" ? socketTypes[dataType].acceptsFrom.length : 0) ||
|
||||
!s.accepts.every((v, i) => v === (direction === "input" ? socketTypes[dataType].acceptsFrom[i] : undefined)) ||
|
||||
!Array.isArray(s.accepts) ||
|
||||
s.accepts.length !==
|
||||
(direction === "input"
|
||||
? socketTypes[dataType].acceptsFrom.length
|
||||
: 0) ||
|
||||
!s.accepts.every(
|
||||
(v, i) =>
|
||||
v ===
|
||||
(direction === "input"
|
||||
? socketTypes[dataType].acceptsFrom[i]
|
||||
: undefined),
|
||||
) ||
|
||||
(socketDefinition.value
|
||||
? !exactKeys(s.defaultValue, ["kind", "value"]) || !finiteJson(s.defaultValue.value)
|
||||
? (() => {
|
||||
try {
|
||||
parameterValue(
|
||||
s.defaultValue,
|
||||
socketDefinition.value,
|
||||
n.id,
|
||||
s.key,
|
||||
);
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
})()
|
||||
: s.defaultValue !== undefined) ||
|
||||
s.visible !== socketDefinition.visible ||
|
||||
s.maxIncomingLinks !== socketDefinition.maxIncomingLinks
|
||||
@@ -206,10 +320,21 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
: descriptor.outputs[s.key].type,
|
||||
authoringType: s.dataType,
|
||||
maxIncomingLinks: s.maxIncomingLinks,
|
||||
defaultValue: socketDefinition.value
|
||||
? structuredClone(s.defaultValue)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
|
||||
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
|
||||
if (n.typeId === "mesh_query") {
|
||||
parameters.visibleDefault = sockets.get(
|
||||
`${n.id}:isVisible`,
|
||||
).defaultValue.value;
|
||||
parameters.frustumCulledDefault = sockets.get(
|
||||
`${n.id}:isFrustumCulled`,
|
||||
).defaultValue.value;
|
||||
}
|
||||
nodes.set(n.id, {
|
||||
ordinal,
|
||||
value: {
|
||||
@@ -230,8 +355,18 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
!object(link) ||
|
||||
!identifier(link.id) ||
|
||||
linkIds.has(link.id) ||
|
||||
!exactKeys(link, ["id", "fromNodeId", "fromSocketId", "toNodeId", "toSocketId", "muted", "extensions"]) ||
|
||||
typeof link.muted !== "boolean" || !object(link.extensions) || !finiteJson(link.extensions)
|
||||
!exactKeys(link, [
|
||||
"id",
|
||||
"fromNodeId",
|
||||
"fromSocketId",
|
||||
"toNodeId",
|
||||
"toSocketId",
|
||||
"muted",
|
||||
"extensions",
|
||||
]) ||
|
||||
typeof link.muted !== "boolean" ||
|
||||
!object(link.extensions) ||
|
||||
!finiteJson(link.extensions)
|
||||
)
|
||||
fail("AUTHORING_LINK", { linkId: link?.id });
|
||||
linkIds.add(link.id);
|
||||
@@ -244,21 +379,33 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
link.toNodeId !== to.node ||
|
||||
from.direction !== "output" ||
|
||||
to.direction !== "input" ||
|
||||
(!link.muted && (incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
|
||||
(!link.muted &&
|
||||
(incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
|
||||
)
|
||||
fail(
|
||||
!link.muted && (incoming.get(link.toSocketId) ?? 0) >= (to?.maxIncomingLinks ?? Infinity)
|
||||
!link.muted &&
|
||||
(incoming.get(link.toSocketId) ?? 0) >=
|
||||
(to?.maxIncomingLinks ?? Infinity)
|
||||
? "AUTHORING_LINK_INCOMING"
|
||||
: "AUTHORING_LINK",
|
||||
!link.muted && (incoming.get(link.toSocketId) ?? 0) >= (to?.maxIncomingLinks ?? Infinity)
|
||||
!link.muted &&
|
||||
(incoming.get(link.toSocketId) ?? 0) >=
|
||||
(to?.maxIncomingLinks ?? Infinity)
|
||||
? { socketId: link.toSocketId }
|
||||
: { linkId: link.id },
|
||||
);
|
||||
const accepted =
|
||||
descriptors[nodes.get(to.node).value.executor.key].inputs[to.key]
|
||||
.accepted.types;
|
||||
const authoringAccepted = socketTypes[nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key].type].acceptsFrom;
|
||||
if (!accepted.includes(from.semanticType) || !authoringAccepted.includes(from.authoringType))
|
||||
const authoringAccepted =
|
||||
socketTypes[
|
||||
nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key]
|
||||
.type
|
||||
].acceptsFrom;
|
||||
if (
|
||||
!accepted.includes(from.semanticType) ||
|
||||
!authoringAccepted.includes(from.authoringType)
|
||||
)
|
||||
fail("AUTHORING_LINK_TYPE", { linkId: link.id });
|
||||
const linkSource = {
|
||||
kind: "link",
|
||||
@@ -299,13 +446,92 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
const base = `nodes[${wireOrdinal}]`;
|
||||
const nodeSource = { kind: "node", nodeId: item.value.id };
|
||||
paths[base] = nodeSource;
|
||||
for (const field of ["id", "state", "executor", "executor.key", "executor.version"])
|
||||
for (const field of [
|
||||
"id",
|
||||
"state",
|
||||
"executor",
|
||||
"executor.key",
|
||||
"executor.version",
|
||||
])
|
||||
paths[`${base}.${field}`] = nodeSource;
|
||||
paths[`${base}.parameters`] = nodeSource;
|
||||
for (const key of Object.keys(item.value.parameters))
|
||||
mapValuePaths(paths, `${base}.parameters.${key}`, { kind: "parameter", nodeId: item.value.id, parameter: key }, item.value.parameters[key]);
|
||||
for (const key of Object.keys(descriptors[item.value.executor.key].inputs)) {
|
||||
const link = raw.links.find((x) => !x.muted && x.toNodeId === item.value.id && sockets.get(x.toSocketId)?.key === key);
|
||||
const parameterSource = (parameter) => ({
|
||||
kind: "parameter",
|
||||
nodeId: item.value.id,
|
||||
parameter,
|
||||
});
|
||||
if (item.value.executor.key === "texture") {
|
||||
const root = `${base}.parameters`;
|
||||
const texture = item.value.parameters.texture;
|
||||
paths[`${root}.residency`] = parameterSource("residency");
|
||||
paths[`${root}.texture`] = nodeSource;
|
||||
paths[`${root}.texture.dimension`] = parameterSource("dimension");
|
||||
paths[`${root}.texture.format`] = parameterSource("format");
|
||||
paths[`${root}.texture.extent`] = parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.kind`] = parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.depthOrArrayLayers`] =
|
||||
parameterSource("depthOrArrayLayers");
|
||||
if (texture.extent.kind === "absolute") {
|
||||
paths[`${root}.texture.extent.width`] =
|
||||
parameterSource("absoluteWidth");
|
||||
paths[`${root}.texture.extent.height`] =
|
||||
parameterSource("absoluteHeight");
|
||||
} else {
|
||||
paths[`${root}.texture.extent.width`] = parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.width.numerator`] = parameterSource(
|
||||
"relativeWidthNumerator",
|
||||
);
|
||||
paths[`${root}.texture.extent.width.denominator`] = parameterSource(
|
||||
"relativeWidthDenominator",
|
||||
);
|
||||
paths[`${root}.texture.extent.height`] =
|
||||
parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.height.numerator`] = parameterSource(
|
||||
"relativeHeightNumerator",
|
||||
);
|
||||
paths[`${root}.texture.extent.height.denominator`] = parameterSource(
|
||||
"relativeHeightDenominator",
|
||||
);
|
||||
}
|
||||
paths[`${root}.texture.mipLevelCount`] =
|
||||
parameterSource("mipLevelCount");
|
||||
paths[`${root}.texture.sampleCount`] = parameterSource("sampleCount");
|
||||
mapValuePaths(
|
||||
paths,
|
||||
`${root}.texture.viewFormats`,
|
||||
parameterSource("viewFormat"),
|
||||
texture.viewFormats,
|
||||
);
|
||||
} else
|
||||
for (const key of Object.keys(item.value.parameters))
|
||||
mapValuePaths(
|
||||
paths,
|
||||
`${base}.parameters.${key}`,
|
||||
item.value.executor.key === "mesh_query" && key.endsWith("Default")
|
||||
? {
|
||||
kind: "input",
|
||||
nodeId: item.value.id,
|
||||
input:
|
||||
key === "visibleDefault" ? "isVisible" : "isFrustumCulled",
|
||||
socketId: `${item.value.id}:${key === "visibleDefault" ? "isVisible" : "isFrustumCulled"}`,
|
||||
unconnected: true,
|
||||
}
|
||||
: parameterSource(
|
||||
item.value.executor.key === "frustum_cull" && key === "camera"
|
||||
? "cameraSelection"
|
||||
: key,
|
||||
),
|
||||
item.value.parameters[key],
|
||||
);
|
||||
for (const key of Object.keys(
|
||||
descriptors[item.value.executor.key].inputs,
|
||||
)) {
|
||||
const link = raw.links.find(
|
||||
(x) =>
|
||||
!x.muted &&
|
||||
x.toNodeId === item.value.id &&
|
||||
sockets.get(x.toSocketId)?.key === key,
|
||||
);
|
||||
const source = linkSources.get(link?.id) ?? {
|
||||
kind: "input",
|
||||
nodeId: item.value.id,
|
||||
|
||||
@@ -3,8 +3,9 @@ import { semanticCatalog } from "./catalog.js";
|
||||
const GROUPS = Object.freeze([
|
||||
["source", "Source"],
|
||||
["compute", "Compute"],
|
||||
["cpu_preparation", "CPU preparation"],
|
||||
["render", "Render / post"],
|
||||
["present", "Present"],
|
||||
["frame", "Frame"],
|
||||
]);
|
||||
|
||||
const title = (typeId) => typeId.replaceAll("_", " ");
|
||||
|
||||
+186
-97
@@ -1,7 +1,6 @@
|
||||
export const GRAPH_ID = "authored_gpu_culling";
|
||||
export const CATALOG_VERSION = 2;
|
||||
export const CATALOG_VERSION = 4;
|
||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||
const oneOf = (...types) => ({ kind: "one_of", types });
|
||||
const i = (type, required = true, authoringType) => ({
|
||||
accepted: typeof type === "string" ? exact(type) : type,
|
||||
required,
|
||||
@@ -25,104 +24,91 @@ const texture = {
|
||||
},
|
||||
};
|
||||
export const semanticCatalog = Object.freeze({
|
||||
surface_target: {
|
||||
mesh: {
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { surface: o("surface_target") },
|
||||
parameters: {},
|
||||
},
|
||||
texture_spec: {
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { spec: o("texture_spec") },
|
||||
parameters: structuredClone(texture),
|
||||
},
|
||||
scene_table: {
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { scene: o("scene_table") },
|
||||
parameters: {},
|
||||
},
|
||||
local_aabb_buffer: {
|
||||
execution: "source",
|
||||
inputs: { scene: i("scene_table") },
|
||||
outputs: { localAabbs: o("local_aabb_buffer") },
|
||||
parameters: {},
|
||||
},
|
||||
camera_frustum: {
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { frustum: o("camera_frustum") },
|
||||
parameters: {},
|
||||
},
|
||||
visibility_flags: {
|
||||
execution: "source",
|
||||
inputs: { scene: i("scene_table") },
|
||||
outputs: {
|
||||
flags: {
|
||||
mesh: o("mesh_data"),
|
||||
localAabbs: o("local_aabb_buffer"),
|
||||
isVisible: {
|
||||
...o("boolean_flag_buffer"),
|
||||
authoringType: "visibility_flag_buffer",
|
||||
},
|
||||
pipelineIndices: o("pipeline_index_stream"),
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
texture: {
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { texture: o("texture") },
|
||||
parameters: {
|
||||
residency: "transient",
|
||||
format: "rgba16_float",
|
||||
dimension: "d2",
|
||||
extentMode: "surface_relative",
|
||||
absoluteWidth: 1,
|
||||
absoluteHeight: 1,
|
||||
relativeWidthNumerator: 1,
|
||||
relativeWidthDenominator: 1,
|
||||
relativeHeightNumerator: 1,
|
||||
relativeHeightDenominator: 1,
|
||||
depthOrArrayLayers: 1,
|
||||
mipLevelCount: 1,
|
||||
sampleCount: "1",
|
||||
viewFormat: "none",
|
||||
},
|
||||
},
|
||||
frustum_cull: {
|
||||
execution: "compute",
|
||||
inputs: {
|
||||
scene: i("scene_table"),
|
||||
mesh: i("mesh_data"),
|
||||
localAabbs: i("local_aabb_buffer"),
|
||||
frustum: i("camera_frustum"),
|
||||
},
|
||||
outputs: {
|
||||
flags: {
|
||||
isFrustumCulled: {
|
||||
...o("boolean_flag_buffer"),
|
||||
authoringType: "frustum_flag_buffer",
|
||||
},
|
||||
},
|
||||
parameters: {},
|
||||
parameters: { cameraSelection: "active" },
|
||||
},
|
||||
mesh_query: {
|
||||
execution: "compute",
|
||||
inputs: {
|
||||
scene: i("scene_table"),
|
||||
mesh: i("mesh_data"),
|
||||
isVisible: i("boolean_flag_buffer", false, "visibility_flag_buffer"),
|
||||
isFrustumCulled: i("boolean_flag_buffer", false, "frustum_flag_buffer"),
|
||||
},
|
||||
outputs: { draws: o("draw_stream") },
|
||||
parameters: {
|
||||
filters: [
|
||||
{ flag: "isVisible", predicate: "required_true" },
|
||||
{ flag: "isFrustumCulled", predicate: "required_false" },
|
||||
],
|
||||
visiblePredicate: "required_true",
|
||||
frustumCulledPredicate: "required_false",
|
||||
},
|
||||
},
|
||||
depth_stencil_config: {
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { config: o("depth_stencil_config") },
|
||||
parameters: {
|
||||
depthCompare: "less_equal",
|
||||
depthWriteEnabled: true,
|
||||
clearDepth: 1,
|
||||
},
|
||||
pipeline_registry: {
|
||||
execution: "cpu_preparation",
|
||||
inputs: { pipelineIndices: i("pipeline_index_stream") },
|
||||
outputs: { activation: o("pipeline_activation") },
|
||||
parameters: {},
|
||||
},
|
||||
legacy_forward: {
|
||||
pipeline: {
|
||||
execution: "render",
|
||||
inputs: {
|
||||
scene: i("scene_table"),
|
||||
mesh: i("mesh_data"),
|
||||
draws: i("draw_stream"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
depthTarget: i(oneOf("texture_spec", "texture")),
|
||||
depthStencil: i("depth_stencil_config"),
|
||||
activation: i("pipeline_activation"),
|
||||
colorTarget: i("texture"),
|
||||
depthTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture"), depth: o("texture") },
|
||||
parameters: { 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] },
|
||||
},
|
||||
fullscreen_copy: {
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: {},
|
||||
@@ -131,7 +117,7 @@ export const semanticCatalog = Object.freeze({
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { exposure: 1 },
|
||||
@@ -140,7 +126,7 @@ export const semanticCatalog = Object.freeze({
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { threshold: 1, knee: 0.5 },
|
||||
@@ -149,7 +135,7 @@ export const semanticCatalog = Object.freeze({
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { direction: [1, 0], radius: 1 },
|
||||
@@ -159,7 +145,7 @@ export const semanticCatalog = Object.freeze({
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
bloom: i("texture"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { intensity: 1 },
|
||||
@@ -168,14 +154,14 @@ export const semanticCatalog = Object.freeze({
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { strength: 2 },
|
||||
},
|
||||
present: {
|
||||
execution: "present",
|
||||
inputs: { surface: i("texture") },
|
||||
frame_out: {
|
||||
execution: "frame",
|
||||
inputs: { color: i("texture") },
|
||||
outputs: {},
|
||||
parameters: {},
|
||||
},
|
||||
@@ -193,15 +179,13 @@ const socketColors = [
|
||||
];
|
||||
export const socketTypes = Object.fromEntries(
|
||||
[
|
||||
"surface_target",
|
||||
"texture_spec",
|
||||
"texture",
|
||||
"scene_table",
|
||||
"mesh_data",
|
||||
"local_aabb_buffer",
|
||||
"camera_frustum",
|
||||
"boolean_flag_buffer",
|
||||
"pipeline_index_stream",
|
||||
"draw_stream",
|
||||
"depth_stencil_config",
|
||||
"pipeline_activation",
|
||||
"visibility_flag_buffer",
|
||||
"frustum_flag_buffer",
|
||||
].map((type, index) => [
|
||||
@@ -213,12 +197,6 @@ export const socketTypes = Object.fromEntries(
|
||||
},
|
||||
]),
|
||||
);
|
||||
socketTypes.surface_target.acceptsFrom = [
|
||||
"surface_target",
|
||||
"texture_spec",
|
||||
"texture",
|
||||
];
|
||||
socketTypes.texture_spec.acceptsFrom = ["texture_spec", "texture"];
|
||||
socketTypes.boolean_flag_buffer.acceptsFrom = [
|
||||
"boolean_flag_buffer",
|
||||
"visibility_flag_buffer",
|
||||
@@ -259,33 +237,138 @@ export const theme = {
|
||||
export const styles = {
|
||||
source: { header: "#3977a8" },
|
||||
compute: { header: "#725a9b" },
|
||||
cpu_preparation: { header: "#8a6d3b" },
|
||||
render: { header: "#426b43" },
|
||||
present: { header: "#a75d37" },
|
||||
frame: { header: "#a75d37" },
|
||||
};
|
||||
const socket = (title, direction, type) => ({
|
||||
const socket = (title, direction, type, value = null) => ({
|
||||
title,
|
||||
direction,
|
||||
type,
|
||||
maxIncomingLinks: direction === "input" ? 1 : 0,
|
||||
visible: true,
|
||||
value: null,
|
||||
showValue: false,
|
||||
value,
|
||||
showValue: value !== null,
|
||||
});
|
||||
const parameterSchema = (value) =>
|
||||
typeof value === "number"
|
||||
? { type: "number", default: { kind: "number", value } }
|
||||
: typeof value === "string"
|
||||
? { type: "string", default: { kind: "string", value } }
|
||||
: typeof value === "boolean"
|
||||
? { type: "boolean", default: { kind: "boolean", value } }
|
||||
: { type: "json", default: { kind: "json", value } };
|
||||
const tagged = (kind, value) => ({ kind, value: structuredClone(value) });
|
||||
const number = (value, minimum, maximum) => ({
|
||||
type: "number",
|
||||
default: tagged("number", value),
|
||||
minimum,
|
||||
maximum,
|
||||
});
|
||||
const enumeration = (value, values) => ({
|
||||
type: "string",
|
||||
default: tagged("string", value),
|
||||
enum: values,
|
||||
});
|
||||
const string = (value) => ({ type: "string", default: tagged("string", value) });
|
||||
const boolean = (value) => ({
|
||||
type: "boolean",
|
||||
default: tagged("boolean", value),
|
||||
});
|
||||
const color = (value) => ({
|
||||
type: "color",
|
||||
default: tagged("color", value),
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
});
|
||||
const json = (value) => ({ type: "json", default: tagged("json", value) });
|
||||
const parameterSchemas = {
|
||||
texture: {
|
||||
residency: enumeration("transient", ["transient", "persistent"]),
|
||||
format: enumeration("rgba16_float", [
|
||||
"rgba8_unorm",
|
||||
"rgba8_unorm_srgb",
|
||||
"bgra8_unorm",
|
||||
"bgra8_unorm_srgb",
|
||||
"rgba16_float",
|
||||
"r32_float",
|
||||
"depth32_float",
|
||||
]),
|
||||
dimension: enumeration("d2", ["d1", "d2", "d3"]),
|
||||
extentMode: enumeration("surface_relative", [
|
||||
"surface_relative",
|
||||
"absolute",
|
||||
]),
|
||||
absoluteWidth: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
absoluteHeight: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeWidthNumerator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeWidthDenominator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeHeightNumerator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeHeightDenominator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
depthOrArrayLayers: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
mipLevelCount: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
sampleCount: enumeration("1", ["1", "4"]),
|
||||
viewFormat: enumeration("none", [
|
||||
"none",
|
||||
"rgba8_unorm",
|
||||
"rgba8_unorm_srgb",
|
||||
"bgra8_unorm",
|
||||
"bgra8_unorm_srgb",
|
||||
"rgba16_float",
|
||||
"r32_float",
|
||||
"depth32_float",
|
||||
]),
|
||||
},
|
||||
mesh: {},
|
||||
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
||||
mesh_query: {
|
||||
visiblePredicate: enumeration("required_true", [
|
||||
"any",
|
||||
"required_true",
|
||||
"required_false",
|
||||
]),
|
||||
frustumCulledPredicate: enumeration("required_false", [
|
||||
"any",
|
||||
"required_true",
|
||||
"required_false",
|
||||
]),
|
||||
},
|
||||
pipeline_registry: {},
|
||||
pipeline: {
|
||||
pipeline: string("gltf_standard"),
|
||||
depthCompare: enumeration("less_equal", [
|
||||
"never",
|
||||
"less",
|
||||
"equal",
|
||||
"less_equal",
|
||||
"greater",
|
||||
"not_equal",
|
||||
"greater_equal",
|
||||
"always",
|
||||
]),
|
||||
depthWriteEnabled: boolean(true),
|
||||
clearDepth: number(1, 0, 1),
|
||||
clearColor: color([0.015, 0.02, 0.03, 1]),
|
||||
},
|
||||
fullscreen_copy: {},
|
||||
tone_map: { exposure: number(1, 0, 32) },
|
||||
bloom_extract: { threshold: number(1, 0, 64), knee: number(0.5, 0, 1) },
|
||||
bloom_blur: {
|
||||
direction: enumeration("horizontal", ["horizontal", "vertical"]),
|
||||
radius: number(1, 1, 16),
|
||||
},
|
||||
bloom_composite: { intensity: number(1, 0, 16) },
|
||||
luminance_edge: { strength: number(2, 0, 16) },
|
||||
frame_out: {},
|
||||
};
|
||||
export const nodeDefinitions = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||
const sockets = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(c.inputs).map(([n, v]) => [
|
||||
n,
|
||||
socket(n, "input", v.authoringType ?? v.accepted.types[0]),
|
||||
socket(
|
||||
n,
|
||||
"input",
|
||||
v.authoringType ?? v.accepted.types[0],
|
||||
key === "mesh_query" && n === "isVisible"
|
||||
? boolean(true)
|
||||
: key === "mesh_query" && n === "isFrustumCulled"
|
||||
? boolean(false)
|
||||
: null,
|
||||
),
|
||||
]),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
@@ -295,12 +378,15 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
]),
|
||||
),
|
||||
},
|
||||
parameters = Object.fromEntries(
|
||||
Object.entries(c.parameters).map(([name, value]) => [
|
||||
name,
|
||||
parameterSchema(value),
|
||||
]),
|
||||
);
|
||||
parameters = parameterSchemas[key];
|
||||
if (
|
||||
!parameters ||
|
||||
Object.keys(parameters).length !== Object.keys(c.parameters).length ||
|
||||
!Object.keys(c.parameters).every((name) =>
|
||||
Object.hasOwn(parameters, name),
|
||||
)
|
||||
)
|
||||
throw new Error(`parameter schema mismatch for ${key}`);
|
||||
return [
|
||||
key,
|
||||
{
|
||||
@@ -314,6 +400,9 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
...Object.keys(parameters).map((parameter) => ({
|
||||
kind: "parameter",
|
||||
parameter,
|
||||
...(key === "frustum_cull" && parameter === "cameraSelection"
|
||||
? { title: "Camera" }
|
||||
: {}),
|
||||
})),
|
||||
...Object.keys(sockets).map((socket) => ({ kind: "socket", socket })),
|
||||
],
|
||||
|
||||
@@ -5,19 +5,16 @@ import { createAddNodeMenu } from "./add-node-menu.js";
|
||||
import { createNodeIdAllocator, spawnRequestedNode } from "./node-spawn.js";
|
||||
|
||||
const spec = [
|
||||
["surface", "surface_target", { x: 40, y: 40 }],
|
||||
["hdr", "texture_spec", { x: 40, y: 170 }],
|
||||
["depth", "texture_spec", { x: 40, y: 300 }],
|
||||
["scene", "scene_table", { x: 40, y: 470 }],
|
||||
["aabbs", "local_aabb_buffer", { x: 290, y: 430 }],
|
||||
["frustum", "camera_frustum", { x: 290, y: 590 }],
|
||||
["visible", "visibility_flags", { x: 290, y: 300 }],
|
||||
["hdr", "texture", { x: 40, y: 170 }],
|
||||
["depth", "texture", { x: 40, y: 300 }],
|
||||
["mesh", "mesh", { x: 40, y: 470 }],
|
||||
["cull", "frustum_cull", { x: 540, y: 480 }],
|
||||
["query", "mesh_query", { x: 790, y: 330 }],
|
||||
["depth_config", "depth_stencil_config", { x: 790, y: 620 }],
|
||||
["forward", "legacy_forward", { x: 1040, y: 290 }],
|
||||
["copy", "fullscreen_copy", { x: 1300, y: 250 }],
|
||||
["present", "present", { x: 1540, y: 250 }],
|
||||
["registry", "pipeline_registry", { x: 790, y: 620 }],
|
||||
["ground", "pipeline", { x: 1040, y: 290 }],
|
||||
["pbr", "pipeline", { x: 1300, y: 290 }],
|
||||
["pbr_double", "pipeline", { x: 1560, y: 290 }],
|
||||
["frame_out", "frame_out", { x: 1820, y: 250 }],
|
||||
];
|
||||
async function seed(root) {
|
||||
await root.setState({
|
||||
@@ -30,22 +27,24 @@ async function seed(root) {
|
||||
for (const [nodeId, nodeType, position] of spec)
|
||||
await root.dispatch({ type: "node.add", nodeId, nodeType, position });
|
||||
const links = [
|
||||
["scene", "scene", "aabbs", "scene"],
|
||||
["scene", "scene", "visible", "scene"],
|
||||
["scene", "scene", "cull", "scene"],
|
||||
["aabbs", "localAabbs", "cull", "localAabbs"],
|
||||
["frustum", "frustum", "cull", "frustum"],
|
||||
["scene", "scene", "query", "scene"],
|
||||
["visible", "flags", "query", "isVisible"],
|
||||
["cull", "flags", "query", "isFrustumCulled"],
|
||||
["scene", "scene", "forward", "scene"],
|
||||
["query", "draws", "forward", "draws"],
|
||||
["hdr", "spec", "forward", "colorTarget"],
|
||||
["depth", "spec", "forward", "depthTarget"],
|
||||
["depth_config", "config", "forward", "depthStencil"],
|
||||
["forward", "color", "copy", "source"],
|
||||
["surface", "surface", "copy", "colorTarget"],
|
||||
["copy", "color", "present", "surface"],
|
||||
["mesh", "mesh", "cull", "mesh"],
|
||||
["mesh", "localAabbs", "cull", "localAabbs"],
|
||||
["mesh", "mesh", "query", "mesh"],
|
||||
["mesh", "isVisible", "query", "isVisible"],
|
||||
["cull", "isFrustumCulled", "query", "isFrustumCulled"],
|
||||
["mesh", "pipelineIndices", "registry", "pipelineIndices"],
|
||||
...["ground", "pbr", "pbr_double"].flatMap((pipeline) => [
|
||||
["mesh", "mesh", pipeline, "mesh"],
|
||||
["query", "draws", pipeline, "draws"],
|
||||
["registry", "activation", pipeline, "activation"],
|
||||
]),
|
||||
["hdr", "texture", "ground", "colorTarget"],
|
||||
["depth", "texture", "ground", "depthTarget"],
|
||||
["ground", "color", "pbr", "colorTarget"],
|
||||
["ground", "depth", "pbr", "depthTarget"],
|
||||
["pbr", "color", "pbr_double", "colorTarget"],
|
||||
["pbr", "depth", "pbr_double", "depthTarget"],
|
||||
["pbr_double", "color", "frame_out", "color"],
|
||||
];
|
||||
for (const [a, as, b, bs] of links) {
|
||||
const id = `${a}_${as}_${b}_${bs}`;
|
||||
@@ -64,34 +63,44 @@ async function seed(root) {
|
||||
}
|
||||
const authored = await root.getState(),
|
||||
depth = authored.nodes.find((node) => node.id === "depth");
|
||||
depth.parameters.texture = {
|
||||
kind: "json",
|
||||
value: {
|
||||
dimension: "d2",
|
||||
format: "depth32_float",
|
||||
extent: {
|
||||
kind: "surface_relative",
|
||||
width: { numerator: 1, denominator: 1 },
|
||||
height: { numerator: 1, denominator: 1 },
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
mipLevelCount: 1,
|
||||
sampleCount: 1,
|
||||
viewFormats: [],
|
||||
},
|
||||
};
|
||||
depth.parameters.format = { kind: "string", value: "depth32_float" };
|
||||
for (const [id, name] of [["ground", "ground_plane"], ["pbr", "gltf_standard"], ["pbr_double", "gltf_standard_double_sided"]])
|
||||
authored.nodes.find((node) => node.id === id).parameters.pipeline = { kind: "string", value: name };
|
||||
await root.setState(authored);
|
||||
}
|
||||
export async function createRenderGraphEditor(canvas) {
|
||||
const allocateId = createNodeIdAllocator();
|
||||
let root, view, menu, destroying, dead = false;
|
||||
const requestAddNode = Object.assign(async (request, point, isCurrent = () => true) => {
|
||||
let typeId;
|
||||
try { typeId = await menu?.open(point); } catch (error) { if (!dead && isCurrent()) console.error(error); return; }
|
||||
if (dead || !isCurrent() || !root || !view) return;
|
||||
const alive = () => !dead && isCurrent();
|
||||
try { await spawnRequestedNode(root, view, request, typeId, allocateId, alive); } catch (error) { if (!dead) console.error(error); }
|
||||
}, { close: () => menu?.close() });
|
||||
let root,
|
||||
view,
|
||||
menu,
|
||||
destroying,
|
||||
dead = false;
|
||||
const requestAddNode = Object.assign(
|
||||
async (request, point, isCurrent = () => true) => {
|
||||
let typeId;
|
||||
try {
|
||||
typeId = await menu?.open(point);
|
||||
} catch (error) {
|
||||
if (!dead && isCurrent()) console.error(error);
|
||||
return;
|
||||
}
|
||||
if (dead || !isCurrent() || !root || !view) return;
|
||||
const alive = () => !dead && isCurrent();
|
||||
try {
|
||||
await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
typeId,
|
||||
allocateId,
|
||||
alive,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!dead) console.error(error);
|
||||
}
|
||||
},
|
||||
{ close: () => menu?.close() },
|
||||
);
|
||||
const host = prepareBrowserHost(canvas, { requestAddNode });
|
||||
const destroy = () =>
|
||||
(destroying ??= (async () => {
|
||||
|
||||
+56
-258
@@ -1,278 +1,76 @@
|
||||
const input = (node, socket) => ({ node, socket });
|
||||
const node = (id, key, parameters = {}, inputs = {}) => ({
|
||||
id,
|
||||
state: "enabled",
|
||||
executor: { key, version: 1 },
|
||||
parameters,
|
||||
inputs,
|
||||
id, state: "enabled", executor: { key, version: 1 }, parameters, inputs,
|
||||
});
|
||||
const texture = (format) => ({
|
||||
const texture = (format, scale = 1) => ({
|
||||
texture: {
|
||||
dimension: "d2",
|
||||
format,
|
||||
extent: {
|
||||
kind: "surface_relative",
|
||||
width: { numerator: 1, denominator: 1 },
|
||||
height: { numerator: 1, denominator: 1 },
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
mipLevelCount: 1,
|
||||
sampleCount: 1,
|
||||
viewFormats: [],
|
||||
dimension: "d2", format,
|
||||
extent: { kind: "surface_relative", width: { numerator: 1, denominator: scale }, height: { numerator: 1, denominator: scale }, depthOrArrayLayers: 1 },
|
||||
mipLevelCount: 1, sampleCount: 1, viewFormats: [],
|
||||
},
|
||||
residency: "transient",
|
||||
});
|
||||
const direct = (graphId, clearColor) => Object.freeze({
|
||||
schemaVersion: 2,
|
||||
graphId,
|
||||
revision: 1,
|
||||
nodes: [
|
||||
node("surface", "surface_target"),
|
||||
node("depth", "texture_spec", texture("depth32_float")),
|
||||
node("scene", "scene_table"),
|
||||
node("visible", "visibility_flags", {}, { scene: input("scene", "scene") }),
|
||||
node("query", "mesh_query", {
|
||||
filters: [
|
||||
{ flag: "isVisible", predicate: "required_true" },
|
||||
{ flag: "isFrustumCulled", predicate: "any" },
|
||||
],
|
||||
}, { scene: input("scene", "scene"), isVisible: input("visible", "flags") }),
|
||||
node("depth_config", "depth_stencil_config", {
|
||||
depthCompare: "less_equal",
|
||||
depthWriteEnabled: true,
|
||||
clearDepth: 1,
|
||||
}),
|
||||
node("forward", "legacy_forward", { clearColor }, {
|
||||
scene: input("scene", "scene"),
|
||||
draws: input("query", "draws"),
|
||||
colorTarget: input("surface", "surface"),
|
||||
depthTarget: input("depth", "spec"),
|
||||
depthStencil: input("depth_config", "config"),
|
||||
}),
|
||||
node("present", "present", {}, { surface: input("forward", "color") }),
|
||||
],
|
||||
});
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1]) => [
|
||||
node("hdr", "texture", texture("rgba16_float")),
|
||||
node("depth", "texture", texture("depth32_float")),
|
||||
node("mesh", "mesh"),
|
||||
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
|
||||
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
|
||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
|
||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), 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 }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||
];
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
||||
const direct = (graphId, clearColor) => graph(graphId, [
|
||||
node("ldr", "texture", texture("rgba8_unorm")),
|
||||
...scene("ldr", clearColor).filter((item) => item.id !== "hdr"),
|
||||
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
|
||||
]);
|
||||
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 hdr = Object.freeze({
|
||||
schemaVersion: 2,
|
||||
graphId: "preset_hdr_fullscreen",
|
||||
revision: 1,
|
||||
nodes: [
|
||||
node("surface", "surface_target"),
|
||||
node("hdr", "texture_spec", texture("rgba16_float")),
|
||||
node("depth", "texture_spec", texture("depth32_float")),
|
||||
node("scene", "scene_table"),
|
||||
node("visible", "visibility_flags", {}, { scene: input("scene", "scene") }),
|
||||
node(
|
||||
"query",
|
||||
"mesh_query",
|
||||
{
|
||||
filters: [
|
||||
{ flag: "isVisible", predicate: "required_true" },
|
||||
{ flag: "isFrustumCulled", predicate: "any" },
|
||||
],
|
||||
},
|
||||
{ scene: input("scene", "scene"), isVisible: input("visible", "flags") },
|
||||
),
|
||||
node("depth_config", "depth_stencil_config", {
|
||||
depthCompare: "less_equal",
|
||||
depthWriteEnabled: true,
|
||||
clearDepth: 1,
|
||||
}),
|
||||
node(
|
||||
"forward",
|
||||
"legacy_forward",
|
||||
{ clearColor: [0.015, 0.02, 0.03, 1] },
|
||||
{
|
||||
scene: input("scene", "scene"),
|
||||
draws: input("query", "draws"),
|
||||
colorTarget: input("hdr", "spec"),
|
||||
depthTarget: input("depth", "spec"),
|
||||
depthStencil: input("depth_config", "config"),
|
||||
},
|
||||
),
|
||||
node(
|
||||
"copy",
|
||||
"fullscreen_copy",
|
||||
{},
|
||||
{
|
||||
source: input("forward", "color"),
|
||||
colorTarget: input("surface", "surface"),
|
||||
},
|
||||
),
|
||||
node("present", "present", {}, { surface: input("copy", "color") }),
|
||||
],
|
||||
});
|
||||
export const culling = Object.freeze((() => {
|
||||
const graph = structuredClone(hdr);
|
||||
graph.graphId = "preset_gpu_culling";
|
||||
graph.nodes.splice(
|
||||
5,
|
||||
0,
|
||||
node("aabbs", "local_aabb_buffer", {}, { scene: input("scene", "scene") }),
|
||||
node("frustum", "camera_frustum"),
|
||||
node("cull", "frustum_cull", {}, {
|
||||
scene: input("scene", "scene"),
|
||||
localAabbs: input("aabbs", "localAabbs"),
|
||||
frustum: input("frustum", "frustum"),
|
||||
}),
|
||||
);
|
||||
const query = graph.nodes.find((x) => x.id === "query");
|
||||
query.parameters.filters[1].predicate = "required_false";
|
||||
query.inputs.isFrustumCulled = input("cull", "flags");
|
||||
return graph;
|
||||
export const hdr = graph("preset_hdr_fullscreen", [
|
||||
...scene("hdr"),
|
||||
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
|
||||
]);
|
||||
export const culling = graph("preset_gpu_culling", (() => {
|
||||
const nodes = structuredClone(hdr.nodes);
|
||||
nodes.splice(3, 0, node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabbs: input("mesh", "localAabbs") }));
|
||||
const query = nodes.find((item) => item.id === "query");
|
||||
query.parameters.frustumCulledPredicate = "required_false";
|
||||
query.inputs.isFrustumCulled = input("cull", "isFrustumCulled");
|
||||
return nodes;
|
||||
})());
|
||||
const postPreset = (graphId, kind) => {
|
||||
const nodes = hdr.nodes.slice(0, 8).map((x) => structuredClone(x));
|
||||
if (kind === "tone")
|
||||
nodes.push(
|
||||
node(
|
||||
"tone",
|
||||
"tone_map",
|
||||
{ exposure: 1 },
|
||||
{
|
||||
source: input("forward", "color"),
|
||||
colorTarget: input("surface", "surface"),
|
||||
},
|
||||
),
|
||||
);
|
||||
const nodes = [node("ldr", "texture", texture("rgba8_unorm")), ...scene("hdr")];
|
||||
let source = "pbr_double";
|
||||
if (kind === "edges") {
|
||||
nodes.splice(1, 0, node("edge_hdr", "texture_spec", texture("rgba16_float")));
|
||||
nodes.push(
|
||||
node(
|
||||
"edges",
|
||||
"luminance_edge",
|
||||
{ strength: 2 },
|
||||
{
|
||||
source: input("forward", "color"),
|
||||
colorTarget: input("edge_hdr", "spec"),
|
||||
},
|
||||
),
|
||||
);
|
||||
nodes.push(
|
||||
node(
|
||||
"tone",
|
||||
"tone_map",
|
||||
{ exposure: 1 },
|
||||
{
|
||||
source: input("edges", "color"),
|
||||
colorTarget: input("surface", "surface"),
|
||||
},
|
||||
),
|
||||
);
|
||||
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
||||
source = "edges";
|
||||
}
|
||||
if (kind === "bloom" || kind === "combined") {
|
||||
const half = {
|
||||
texture: {
|
||||
...texture("rgba16_float").texture,
|
||||
extent: {
|
||||
kind: "surface_relative",
|
||||
width: { numerator: 1, denominator: 2 },
|
||||
height: { numerator: 1, denominator: 2 },
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
},
|
||||
residency: "transient",
|
||||
};
|
||||
nodes.splice(
|
||||
1,
|
||||
0,
|
||||
node("half_a", "texture_spec", structuredClone(half)),
|
||||
node("half_b", "texture_spec", structuredClone(half)),
|
||||
node("half_c", "texture_spec", structuredClone(half)),
|
||||
node("composite_hdr", "texture_spec", texture("rgba16_float")),
|
||||
);
|
||||
nodes.splice(1, 0,
|
||||
node("half_a", "texture", texture("rgba16_float", 2)), node("half_b", "texture", texture("rgba16_float", 2)),
|
||||
node("half_c", "texture", texture("rgba16_float", 2)), node("composite_hdr", "texture", texture("rgba16_float")));
|
||||
nodes.push(
|
||||
node(
|
||||
"extract",
|
||||
"bloom_extract",
|
||||
{ threshold: 1, knee: 0.5 },
|
||||
{
|
||||
source: input("forward", "color"),
|
||||
colorTarget: input("half_a", "spec"),
|
||||
},
|
||||
),
|
||||
node("extract", "bloom_extract", { threshold: 1, knee: 0.5 }, { source: input("pbr_double", "color"), colorTarget: input("half_a", "texture") }),
|
||||
node("blur_h", "bloom_blur", { direction: [1, 0], radius: 1 }, { source: input("extract", "color"), colorTarget: input("half_b", "texture") }),
|
||||
node("blur_v", "bloom_blur", { direction: [0, 1], radius: 1 }, { source: input("blur_h", "color"), colorTarget: input("half_c", "texture") }),
|
||||
node("composite", "bloom_composite", { intensity: 0.8 }, { source: input("pbr_double", "color"), bloom: input("blur_v", "color"), colorTarget: input("composite_hdr", "texture") }),
|
||||
);
|
||||
nodes.push(
|
||||
node(
|
||||
"blur_h",
|
||||
"bloom_blur",
|
||||
{ direction: [1, 0], radius: 1 },
|
||||
{
|
||||
source: input("extract", "color"),
|
||||
colorTarget: input("half_b", "spec"),
|
||||
},
|
||||
),
|
||||
);
|
||||
nodes.push(
|
||||
node(
|
||||
"blur_v",
|
||||
"bloom_blur",
|
||||
{ direction: [0, 1], radius: 1 },
|
||||
{
|
||||
source: input("blur_h", "color"),
|
||||
colorTarget: input("half_c", "spec"),
|
||||
},
|
||||
),
|
||||
);
|
||||
nodes.push(
|
||||
node(
|
||||
"composite",
|
||||
"bloom_composite",
|
||||
{ intensity: 0.8 },
|
||||
{
|
||||
source: input("forward", "color"),
|
||||
bloom: input("blur_v", "color"),
|
||||
colorTarget: input("composite_hdr", "spec"),
|
||||
},
|
||||
),
|
||||
);
|
||||
let toneSource = "composite";
|
||||
source = "composite";
|
||||
if (kind === "combined") {
|
||||
nodes.splice(1, 0, node("edge_hdr", "texture_spec", texture("rgba16_float")));
|
||||
nodes.push(
|
||||
node(
|
||||
"edges",
|
||||
"luminance_edge",
|
||||
{ strength: 2 },
|
||||
{
|
||||
source: input("composite", "color"),
|
||||
colorTarget: input("edge_hdr", "spec"),
|
||||
},
|
||||
),
|
||||
);
|
||||
toneSource = "edges";
|
||||
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
||||
source = "edges";
|
||||
}
|
||||
nodes.push(
|
||||
node(
|
||||
"tone",
|
||||
"tone_map",
|
||||
{ exposure: 1 },
|
||||
{
|
||||
source: input(toneSource, "color"),
|
||||
colorTarget: input("surface", "surface"),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
const last = nodes.at(-1);
|
||||
nodes.push(
|
||||
node("present", "present", {}, { surface: input(last.id, "color") }),
|
||||
);
|
||||
return Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
||||
nodes.push(node("tone", "tone_map", { exposure: 1 }, { source: input(source, "color"), colorTarget: input("ldr", "texture") }));
|
||||
nodes.push(node("frame_out", "frame_out", {}, { color: input("tone", "color") }));
|
||||
return graph(graphId, nodes);
|
||||
};
|
||||
export const tone = postPreset("preset_tone", "tone"),
|
||||
edges = postPreset("preset_edges", "edges"),
|
||||
bloom = postPreset("preset_bloom", "bloom"),
|
||||
combined = postPreset("preset_combined", "combined");
|
||||
export const renderGraphPresets = Object.freeze({
|
||||
midnight,
|
||||
ember,
|
||||
hdr,
|
||||
culling,
|
||||
tone,
|
||||
edges,
|
||||
bloom,
|
||||
combined,
|
||||
});
|
||||
export const tone = postPreset("preset_tone", "tone");
|
||||
export const edges = postPreset("preset_edges", "edges");
|
||||
export const bloom = postPreset("preset_bloom", "bloom");
|
||||
export const combined = postPreset("preset_combined", "combined");
|
||||
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, edges, bloom, combined });
|
||||
|
||||
+156
-36
@@ -1,13 +1,26 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { addNodeItems, moveAddNodeSelection, searchAddNodeItems } from "../static/render-graph/add-node-menu.js";
|
||||
import { createNodeIdAllocator, spawnRequestedNode } from "../static/render-graph/node-spawn.js";
|
||||
import {
|
||||
addNodeItems,
|
||||
moveAddNodeSelection,
|
||||
searchAddNodeItems,
|
||||
} from "../static/render-graph/add-node-menu.js";
|
||||
import {
|
||||
createNodeIdAllocator,
|
||||
spawnRequestedNode,
|
||||
} from "../static/render-graph/node-spawn.js";
|
||||
|
||||
test("add-node model contains all 17 catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 17);
|
||||
assert.deepEqual([...new Set(addNodeItems.map((item) => item.group))], ["Source", "Compute", "Render / post", "Present"]);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 17);
|
||||
assert.deepEqual(searchAddNodeItems("tone render").map((item) => item.typeId), ["tone_map"]);
|
||||
test("add-node model contains all 13 catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 13);
|
||||
assert.deepEqual(
|
||||
[...new Set(addNodeItems.map((item) => item.group))],
|
||||
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
||||
);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 13);
|
||||
assert.deepEqual(
|
||||
searchAddNodeItems("tone render").map((item) => item.typeId),
|
||||
["tone_map"],
|
||||
);
|
||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
||||
});
|
||||
|
||||
@@ -21,13 +34,19 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
|
||||
const values = ["a-a", "a-a", "b-b"];
|
||||
const allocate = createNodeIdAllocator(() => values.shift());
|
||||
assert.equal(allocate(["node_aa"]), "node_bb");
|
||||
assert.throws(() => createNodeIdAllocator(() => "bad id")([]), /Unable to allocate/);
|
||||
assert.throws(
|
||||
() => createNodeIdAllocator(() => "bad id")([]),
|
||||
/Unable to allocate/,
|
||||
);
|
||||
});
|
||||
|
||||
test("all 17 types spawn with exact position, current version and generated ID", async () => {
|
||||
let revision = 5, expectedType;
|
||||
test("all 13 types spawn with exact position, current version and generated ID", async () => {
|
||||
let revision = 5,
|
||||
expectedType;
|
||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
||||
const root = { getState: async () => ({ version: 91, nodes: [{ id: "existing" }] }) };
|
||||
const root = {
|
||||
getState: async () => ({ version: 91, nodes: [{ id: "existing" }] }),
|
||||
};
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||
addNode: async (params, options) => {
|
||||
@@ -38,41 +57,89 @@ test("all 17 types spawn with exact position, current version and generated ID",
|
||||
},
|
||||
};
|
||||
let id = 0;
|
||||
const allocate = createNodeIdAllocator(() => `00000000-0000-0000-0000-${String(++id).padStart(12, "0")}`);
|
||||
const allocate = createNodeIdAllocator(
|
||||
() => `00000000-0000-0000-0000-${String(++id).padStart(12, "0")}`,
|
||||
);
|
||||
for (const item of addNodeItems) {
|
||||
expectedType = item.typeId;
|
||||
assert.equal(await spawnRequestedNode(root, view, request, item.typeId, allocate), true);
|
||||
assert.equal(
|
||||
await spawnRequestedNode(root, view, request, item.typeId, allocate),
|
||||
true,
|
||||
);
|
||||
}
|
||||
revision = 6;
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", allocate), false);
|
||||
assert.equal(
|
||||
await spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("spawn rechecks composition after getState and propagates add errors", async () => {
|
||||
let revision = 2;
|
||||
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 2 } };
|
||||
const root = { getState: async () => { revision++; return { version: 3, nodes: [] }; } };
|
||||
const root = {
|
||||
getState: async () => {
|
||||
revision++;
|
||||
return { version: 3, nodes: [] };
|
||||
},
|
||||
};
|
||||
const allocate = createNodeIdAllocator(() => "a");
|
||||
const view = { getHostSnapshot: () => ({ compositionRevision: revision }), addNode: async () => { throw Error("must not add"); } };
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", allocate), false);
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||
addNode: async () => {
|
||||
throw Error("must not add");
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||
false,
|
||||
);
|
||||
revision = 2;
|
||||
root.getState = async () => ({ version: 3, nodes: [] });
|
||||
await assert.rejects(spawnRequestedNode(root, view, request, "tone_map", allocate), /must not add/);
|
||||
await assert.rejects(
|
||||
spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||
/must not add/,
|
||||
);
|
||||
});
|
||||
|
||||
test("spawn cancels when a pending getState becomes mutated or dead", async () => {
|
||||
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 2 } };
|
||||
let resolveState, revision = 2, alive = true, adds = 0;
|
||||
const root = { getState: () => new Promise((resolve) => { resolveState = resolve; }) };
|
||||
let resolveState,
|
||||
revision = 2,
|
||||
alive = true,
|
||||
adds = 0;
|
||||
const root = {
|
||||
getState: () =>
|
||||
new Promise((resolve) => {
|
||||
resolveState = resolve;
|
||||
}),
|
||||
};
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||
addNode: async () => { adds++; },
|
||||
addNode: async () => {
|
||||
adds++;
|
||||
},
|
||||
};
|
||||
const pendingMutation = spawnRequestedNode(root, view, request, "tone_map", () => "node_a", () => alive);
|
||||
const pendingMutation = spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_a",
|
||||
() => alive,
|
||||
);
|
||||
revision++;
|
||||
resolveState({ version: 1, nodes: [] });
|
||||
assert.equal(await pendingMutation, false);
|
||||
revision = 2;
|
||||
const pendingDestroy = spawnRequestedNode(root, view, request, "tone_map", () => "node_b", () => alive);
|
||||
const pendingDestroy = spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_b",
|
||||
() => alive,
|
||||
);
|
||||
alive = false;
|
||||
resolveState({ version: 1, nodes: [] });
|
||||
assert.equal(await pendingDestroy, false);
|
||||
@@ -80,14 +147,27 @@ test("spawn cancels when a pending getState becomes mutated or dead", async () =
|
||||
});
|
||||
|
||||
test("spawn has a final liveness guard after ID allocation", async () => {
|
||||
let alive = true, adds = 0;
|
||||
let alive = true,
|
||||
adds = 0;
|
||||
const request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
||||
const root = { getState: async () => ({ version: 1, nodes: [] }) };
|
||||
const view = { getHostSnapshot: () => ({ compositionRevision: 1 }), addNode: async () => { adds++; } };
|
||||
const result = await spawnRequestedNode(root, view, request, "tone_map", () => {
|
||||
alive = false;
|
||||
return "node_reserved";
|
||||
}, () => alive);
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: 1 }),
|
||||
addNode: async () => {
|
||||
adds++;
|
||||
},
|
||||
};
|
||||
const result = await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => {
|
||||
alive = false;
|
||||
return "node_reserved";
|
||||
},
|
||||
() => alive,
|
||||
);
|
||||
assert.equal(result, false);
|
||||
assert.equal(adds, 0);
|
||||
});
|
||||
@@ -95,19 +175,59 @@ test("spawn has a final liveness guard after ID allocation", async () => {
|
||||
test("spawn suppresses teardown RPC rejections but propagates genuine live add errors", async () => {
|
||||
const request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
||||
let alive = true;
|
||||
const view = { getHostSnapshot: () => ({ compositionRevision: 1 }), addNode: async () => {} };
|
||||
const root = { getState: async () => { alive = false; throw Error("detached state"); } };
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", () => "node_a", () => alive), false);
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: 1 }),
|
||||
addNode: async () => {},
|
||||
};
|
||||
const root = {
|
||||
getState: async () => {
|
||||
alive = false;
|
||||
throw Error("detached state");
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_a",
|
||||
() => alive,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
alive = true;
|
||||
root.getState = async () => ({ version: 1, nodes: [] });
|
||||
view.addNode = async () => { alive = false; throw Error("detached add"); };
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", () => "node_b", () => alive), false);
|
||||
view.addNode = async () => {
|
||||
alive = false;
|
||||
throw Error("detached add");
|
||||
};
|
||||
assert.equal(
|
||||
await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_b",
|
||||
() => alive,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
alive = true;
|
||||
view.addNode = async () => { throw Error("live add failure"); };
|
||||
view.addNode = async () => {
|
||||
throw Error("live add failure");
|
||||
};
|
||||
await assert.rejects(
|
||||
spawnRequestedNode(root, view, request, "tone_map", () => "node_c", () => alive),
|
||||
spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_c",
|
||||
() => alive,
|
||||
),
|
||||
/live add failure/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,6 @@ test("procedural cube and sphere have complete indexed vertex streams",()=>{cons
|
||||
test("every cube triangle has counter-clockwise outward winding",()=>{const g=createCubeGeometry();for(let i=0;i<g.indices.length;i+=3){const ids=g.indices.slice(i,i+3),p=ids.map(id=>g.positions.slice(id*3,id*3+3)),a=p[1].map((v,j)=>v-p[0][j]),b=p[2].map((v,j)=>v-p[0][j]),cross=[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]],normal=g.normals.slice(ids[0]*3,ids[0]*3+3);assert.ok(cross.reduce((sum,v,j)=>sum+v*normal[j],0)>0)}});
|
||||
test("loadouts return fresh deterministic GLBs with one mesh and nine nodes",async()=>{for(const name of ["cubes","spheres"]){const a=await loadDemoLoadout(name),b=await loadDemoLoadout(name);assert.notStrictEqual(a,b);assert.deepEqual(new Uint8Array(a),new Uint8Array(b));const json=parseGlb(a);assert.equal(json.meshes.length,1);assert.equal(json.nodes.length,9);assert.ok(json.nodes.every(n=>n.mesh===0));assert.deepEqual(json.meshes[0].primitives[0].attributes,{POSITION:0,NORMAL:1,TEXCOORD_0:2});assert.equal(json.accessors[3].componentType,5125);}});
|
||||
test("Phase 6 gallery is deterministic and covers core PBR shader semantics",async()=>{const a=createMaterialGalleryGlb(),b=await loadDemoLoadout("materials");assert.deepEqual(new Uint8Array(a),new Uint8Array(b));const json=parseGlb(a);assert.equal(json.asset.generator,"yawn-phase6-pbr-gallery");assert.deepEqual(json.extensionsUsed,["KHR_materials_ior"]);assert.equal(json.materials.length,16);assert.equal(json.nodes.length,16);assert.equal(json.images.length,3);assert.ok(json.images.every(image=>image.mimeType==="image/png"&&image.bufferView!==undefined));assert.ok(json.meshes.every(mesh=>JSON.stringify(mesh.primitives[0].attributes)===JSON.stringify({POSITION:0,NORMAL:1,TEXCOORD_0:2})));assert.deepEqual(json.materials.slice(0,4).map(x=>x.pbrMetallicRoughness.roughnessFactor),[.08,.3,.6,1]);assert.ok(json.materials.slice(0,4).every(x=>x.pbrMetallicRoughness.metallicFactor===0));assert.ok(json.materials.slice(4,8).every(x=>x.pbrMetallicRoughness.metallicFactor===1));assert.deepEqual(json.materials.slice(8,11).map(x=>x.extensions.KHR_materials_ior.ior),[1,1.5,2]);assert.equal(json.materials[11].normalTexture.index,2);assert.equal(json.materials[12].occlusionTexture.index,1);assert.equal(json.materials[13].emissiveTexture.index,0);assert.equal(json.materials[14].alphaMode,"MASK");assert.equal(json.materials[15].doubleSided,true);assert.ok(json.nodes[15].scale[0]<0);assert.equal(loadouts.materials.label,"Phase 6 deterministic PBR gallery");});
|
||||
test("loadout dropdown preserves legacy scenes and exposes the material gallery",async()=>{const html=await (await import("node:fs/promises")).readFile(new URL("../static/index.html",import.meta.url),"utf8");for(const id of ["cubes","spheres","materials","manor","sponza"])assert.match(html,new RegExp(`<option value="${id}">`));});
|
||||
test("loadout dropdown preserves every demo scene and exposes the material gallery",async()=>{const html=await (await import("node:fs/promises")).readFile(new URL("../static/index.html",import.meta.url),"utf8");for(const id of ["cubes","spheres","materials","manor","sponza"])assert.match(html,new RegExp(`<option value="${id}">`));});
|
||||
test("LFS pointers and HTTP failures are explicit and stable",async()=>{const pointer=new TextEncoder().encode("version https://git-lfs.github.com/spec/v1\noid sha256:abc\n").buffer;assert.equal(isGitLfsPointer(pointer),true);await assert.rejects(loadDemoLoadout("manor",{fetchImpl:async()=>({ok:true,arrayBuffer:async()=>pointer})}),e=>e instanceof LoadoutError&&e.code==="LOADOUT_LFS_POINTER");await assert.rejects(loadDemoLoadout("sponza",{fetchImpl:async()=>({ok:false,status:404})}),e=>e.code==="LOADOUT_HTTP"&&/404/.test(e.message));});
|
||||
test("GLB encoder rejects non-finite and out-of-range geometry",()=>{const valid=createCubeGeometry();assert.throws(()=>encodeGeometryGlb({...valid,positions:[...valid.positions.slice(0,-1),NaN]}),/Invalid/);assert.throws(()=>encodeGeometryGlb({...valid,indices:[...valid.indices,0,1,999]}),/Invalid/)});
|
||||
|
||||
@@ -8,19 +8,41 @@ import { build } from "vite";
|
||||
import { fxNodeComposition } from "../static/render-graph/catalog.js";
|
||||
|
||||
test("production render graph composition passes fxnode's public validator", async () => {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "yawn-fxnode-validator-"));
|
||||
const directory = await mkdtemp(
|
||||
path.join(tmpdir(), "yawn-fxnode-validator-"),
|
||||
);
|
||||
try {
|
||||
const entry = path.join(directory, "entry.js");
|
||||
await writeFile(entry, `export { validateFxNodeComposition } from ${JSON.stringify(pathToFileURL(path.resolve("vendor/fxnode/src/index.ts")).href)};`);
|
||||
await writeFile(
|
||||
entry,
|
||||
`export { validateFxNodeComposition } from ${JSON.stringify(pathToFileURL(path.resolve("vendor/fxnode/src/index.ts")).href)};`,
|
||||
);
|
||||
await build({
|
||||
configFile: false,
|
||||
logLevel: "silent",
|
||||
build: { lib: { entry, formats: ["es"], fileName: "validator" }, outDir: directory, emptyOutDir: false },
|
||||
build: {
|
||||
lib: { entry, formats: ["es"], fileName: "validator" },
|
||||
outDir: directory,
|
||||
emptyOutDir: false,
|
||||
},
|
||||
});
|
||||
const { validateFxNodeComposition } = await import(`${pathToFileURL(path.join(directory, "validator.js")).href}?${Date.now()}`);
|
||||
const { validateFxNodeComposition } = await import(
|
||||
`${pathToFileURL(path.join(directory, "validator.js")).href}?${Date.now()}`
|
||||
);
|
||||
const result = validateFxNodeComposition(fxNodeComposition);
|
||||
assert.equal(result.ok, true, result.ok ? undefined : JSON.stringify(result.issues, null, 2));
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 17);
|
||||
assert.equal(
|
||||
result.ok,
|
||||
true,
|
||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||
);
|
||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||
assert.equal(fxNodeComposition.version, 4);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 13);
|
||||
assert.ok(
|
||||
Object.values(fxNodeComposition.nodes).every(
|
||||
(definition) => definition.migrations.length === 0,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -41,16 +41,23 @@ function fixture() {
|
||||
]),
|
||||
),
|
||||
sockets: [
|
||||
...Object.entries(d.inputs).map(([key, x]) => ({
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
direction: "input",
|
||||
dataType: x.authoringType ?? x.accepted.types[0],
|
||||
label: key,
|
||||
accepts: socketTypes[x.authoringType ?? x.accepted.types[0]].acceptsFrom,
|
||||
visible: true,
|
||||
maxIncomingLinks: 1,
|
||||
})),
|
||||
...Object.entries(d.inputs).map(([key, x]) => {
|
||||
const socket = definition.sockets[key];
|
||||
return {
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
direction: "input",
|
||||
dataType: x.authoringType ?? x.accepted.types[0],
|
||||
label: socket.title,
|
||||
accepts:
|
||||
socketTypes[x.authoringType ?? x.accepted.types[0]].acceptsFrom,
|
||||
...(socket.value
|
||||
? { defaultValue: structuredClone(socket.value.default) }
|
||||
: {}),
|
||||
visible: socket.visible,
|
||||
maxIncomingLinks: socket.maxIncomingLinks,
|
||||
};
|
||||
}),
|
||||
...Object.entries(d.outputs).map(([key]) => ({
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
@@ -87,26 +94,22 @@ function fixture() {
|
||||
}
|
||||
test("catalog exhaustively mirrors all current contracts", () => {
|
||||
assert.deepEqual(
|
||||
Object.keys(semanticCatalog).sort(),
|
||||
Object.keys(semanticCatalog),
|
||||
[
|
||||
"surface_target",
|
||||
"texture_spec",
|
||||
"scene_table",
|
||||
"local_aabb_buffer",
|
||||
"camera_frustum",
|
||||
"visibility_flags",
|
||||
"mesh",
|
||||
"texture",
|
||||
"frustum_cull",
|
||||
"mesh_query",
|
||||
"depth_stencil_config",
|
||||
"legacy_forward",
|
||||
"pipeline_registry",
|
||||
"pipeline",
|
||||
"fullscreen_copy",
|
||||
"tone_map",
|
||||
"bloom_extract",
|
||||
"bloom_blur",
|
||||
"bloom_composite",
|
||||
"luminance_edge",
|
||||
"present",
|
||||
].sort(),
|
||||
"frame_out",
|
||||
],
|
||||
);
|
||||
for (const c of Object.values(semanticCatalog)) {
|
||||
assert.ok(c.execution);
|
||||
@@ -114,6 +117,161 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
||||
assert.ok(c.outputs);
|
||||
assert.ok(c.parameters);
|
||||
}
|
||||
for (const [key, contract] of Object.entries(semanticCatalog))
|
||||
assert.deepEqual(
|
||||
Object.keys(nodeDefinitions[key].parameters).sort(),
|
||||
Object.keys(contract.parameters).sort(),
|
||||
key,
|
||||
);
|
||||
assert.equal(CATALOG_VERSION, 4);
|
||||
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
|
||||
pipeline: {
|
||||
type: "string",
|
||||
default: { kind: "string", value: "gltf_standard" },
|
||||
},
|
||||
depthCompare: {
|
||||
type: "string",
|
||||
default: { kind: "string", value: "less_equal" },
|
||||
enum: ["never", "less", "equal", "less_equal", "greater", "not_equal", "greater_equal", "always"],
|
||||
},
|
||||
depthWriteEnabled: {
|
||||
type: "boolean",
|
||||
default: { kind: "boolean", value: true },
|
||||
},
|
||||
clearDepth: {
|
||||
type: "number",
|
||||
default: { kind: "number", value: 1 },
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
clearColor: {
|
||||
type: "color",
|
||||
default: { kind: "color", value: [0.015, 0.02, 0.03, 1] },
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(nodeDefinitions.bloom_blur.parameters.direction.enum, [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
]);
|
||||
assert.deepEqual(nodeDefinitions.texture.parameters.residency.enum, [
|
||||
"transient",
|
||||
"persistent",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
nodeDefinitions.frustum_cull.parameters.cameraSelection.enum,
|
||||
["active"],
|
||||
);
|
||||
assert.deepEqual(nodeDefinitions.mesh_query.sockets.isVisible.value.default, {
|
||||
kind: "boolean",
|
||||
value: true,
|
||||
});
|
||||
assert.equal(nodeDefinitions.mesh_query.sockets.isVisible.showValue, true);
|
||||
});
|
||||
|
||||
test("adapter validates and exactly lowers canonical pipeline controls and blur direction", () => {
|
||||
const x = fixture();
|
||||
const pipeline = x.nodes.find((node) => node.id === "ground");
|
||||
assert.equal(pipeline.parameters.clearColor.kind, "color");
|
||||
assert.deepEqual(
|
||||
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "ground").parameters,
|
||||
{
|
||||
pipeline: "ground_plane",
|
||||
depthCompare: "less_equal",
|
||||
depthWriteEnabled: true,
|
||||
clearDepth: 1,
|
||||
clearColor: [0.015, 0.02, 0.03, 1],
|
||||
},
|
||||
);
|
||||
const schema = nodeDefinitions.bloom_blur.parameters;
|
||||
const blur = structuredClone(x.nodes.find((node) => node.id === "frame_out"));
|
||||
blur.id = "blur";
|
||||
blur.typeId = "bloom_blur";
|
||||
blur.parameters = {
|
||||
direction: { kind: "string", value: "vertical" },
|
||||
radius: structuredClone(schema.radius.default),
|
||||
};
|
||||
blur.sockets = Object.entries(nodeDefinitions.bloom_blur.sockets).map(
|
||||
([key, socket]) => ({
|
||||
key,
|
||||
id: `blur:${key}`,
|
||||
label: socket.title,
|
||||
direction: socket.direction,
|
||||
dataType: socket.type,
|
||||
accepts:
|
||||
socket.direction === "input"
|
||||
? socketTypes[socket.type].acceptsFrom
|
||||
: [],
|
||||
maxIncomingLinks: socket.maxIncomingLinks,
|
||||
visible: socket.visible,
|
||||
}),
|
||||
);
|
||||
x.nodes.push(blur);
|
||||
assert.deepEqual(
|
||||
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "blur").parameters
|
||||
.direction,
|
||||
[0, 1],
|
||||
);
|
||||
pipeline.parameters.clearColor.value[0] = 2;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
pipeline.parameters.clearColor.value = [0, 0, 0];
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
pipeline.parameters.clearColor.value = [0, 0, Number.NaN, 1];
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
const texture = x.nodes.find((node) => node.id === "hdr");
|
||||
texture.parameters.residency.value = "unknown";
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
texture.parameters.residency.value = "transient";
|
||||
pipeline.parameters.clearDepth.value = -1;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
});
|
||||
|
||||
test("adapter validates and lowers disconnected query socket defaults", () => {
|
||||
const x = fixture();
|
||||
const query = x.nodes.find((node) => node.id === "query");
|
||||
const visible = query.sockets.find((socket) => socket.key === "isVisible");
|
||||
visible.defaultValue.value = false;
|
||||
const ir = adaptFxNodeSnapshot(x);
|
||||
assert.equal(visible.defaultValue.value, false);
|
||||
const parameters = ir.nodes.find((node) => node.id === "query").parameters;
|
||||
assert.equal(parameters.isVisible, undefined);
|
||||
assert.equal(parameters.visibleDefault, false);
|
||||
assert.equal(parameters.frustumCulledDefault, false);
|
||||
visible.defaultValue = { kind: "number", value: 0 };
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_SOCKET",
|
||||
);
|
||||
delete visible.defaultValue;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_SOCKET",
|
||||
);
|
||||
});
|
||||
test("adapter lowers the authoring-safe camera selector to the Rust wire field", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const parameters = ir.nodes.find((node) => node.id === "cull").parameters;
|
||||
assert.deepEqual(parameters, { camera: "active" });
|
||||
assert.equal(parameters.cameraSelection, undefined);
|
||||
});
|
||||
test("adapter deterministically emits the canonical schema, permits repeated types, omits muted links and maps sources", () => {
|
||||
const x = fixture(),
|
||||
@@ -123,16 +281,14 @@ test("adapter deterministically emits the canonical schema, permits repeated typ
|
||||
assert.deepEqual(adaptFxNodeSnapshot(x, 7), a);
|
||||
assert.equal(a.schemaVersion, 2);
|
||||
assert.equal(a.graphId, GRAPH_ID);
|
||||
assert.equal(
|
||||
a.nodes.filter((n) => n.executor.key === "texture_spec").length,
|
||||
2,
|
||||
);
|
||||
assert.equal(a.nodes.filter((n) => n.executor.key === "texture").length, 2);
|
||||
assert.ok(
|
||||
Object.values(getSourceMap(a)).some((source) => source.input === "source"),
|
||||
Object.values(getSourceMap(a)).some((source) => source.input === "color"),
|
||||
);
|
||||
x.links.find((l) => l.id === "l_forward_color_copy_source").muted = true;
|
||||
x.links.find((l) => l.id === "l_pbr_double_color_frame_out_color").muted = true;
|
||||
assert.equal(
|
||||
adaptFxNodeSnapshot(x, 8).nodes.find((n) => n.id === "copy").inputs.source,
|
||||
adaptFxNodeSnapshot(x, 8).nodes.find((n) => n.id === "frame_out").inputs
|
||||
.color,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
@@ -146,50 +302,165 @@ test("adapter rejects hostile shape, IDs, duplicates, catalog, sockets and type
|
||||
);
|
||||
};
|
||||
reject((x) => (x.graphId = "bad"), "AUTHORING_CATALOG");
|
||||
reject((x) => (x.catalogVersion = 2), "AUTHORING_CATALOG");
|
||||
reject((x) => (x.nodes[0].id = "bad id"), "AUTHORING_ID");
|
||||
reject((x) => (x.nodes[1].id = x.nodes[0].id), "AUTHORING_ID_DUPLICATE");
|
||||
reject((x) => (x.nodes[0].typeId = "wat"), "AUTHORING_NODE_TYPE");
|
||||
reject((x) => (x.nodes[0].sockets = []), "AUTHORING_SOCKET_SET");
|
||||
reject((x) => (x.links[0].toSocketId = "missing:x"), "AUTHORING_LINK");
|
||||
reject((x) => {
|
||||
const link = x.links.find((l) => l.toSocketId === "copy:source");
|
||||
link.fromNodeId = "scene";
|
||||
link.fromSocketId = "scene:scene";
|
||||
const link = x.links.find((l) => l.toSocketId === "frame_out:color");
|
||||
link.fromNodeId = "mesh";
|
||||
link.fromSocketId = "mesh:mesh";
|
||||
}, "AUTHORING_LINK_TYPE");
|
||||
});
|
||||
test("adapter counts only active incoming links and reports socket overflow", () => {
|
||||
const x = fixture();
|
||||
const active = x.links.find((link) => link.toSocketId === "copy:source");
|
||||
x.links.push({ ...structuredClone(active), id: "muted_duplicate", muted: true });
|
||||
const active = x.links.find((link) => link.toSocketId === "frame_out:color");
|
||||
x.links.push({
|
||||
...structuredClone(active),
|
||||
id: "muted_duplicate",
|
||||
muted: true,
|
||||
});
|
||||
assert.doesNotThrow(() => adaptFxNodeSnapshot(x));
|
||||
x.links.push({ ...structuredClone(active), id: "active_overflow" });
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_LINK_INCOMING" && error.details.socketId === "copy:source",
|
||||
(error) =>
|
||||
error.code === "AUTHORING_LINK_INCOMING" &&
|
||||
error.details.socketId === "frame_out:color",
|
||||
);
|
||||
});
|
||||
test("source map covers Rust fields, nested values, every input and is deeply frozen", () => {
|
||||
const snapshot = fixture();
|
||||
snapshot.links.find((link) => link.toSocketId === "copy:source").muted = true;
|
||||
const ir = adaptFxNodeSnapshot(snapshot, 9), map = getSourceMap(ir);
|
||||
for (const path of ["schemaVersion", "graphId", "revision", "nodes", "nodes[0].id", "nodes[0].state", "nodes[0].executor.key", "nodes[0].executor.version", "nodes[0].parameters", "nodes[0].inputs"])
|
||||
snapshot.links.find((link) => link.toSocketId === "frame_out:color").muted =
|
||||
true;
|
||||
const ir = adaptFxNodeSnapshot(snapshot, 9),
|
||||
map = getSourceMap(ir);
|
||||
for (const path of [
|
||||
"schemaVersion",
|
||||
"graphId",
|
||||
"revision",
|
||||
"nodes",
|
||||
"nodes[0].id",
|
||||
"nodes[0].state",
|
||||
"nodes[0].executor.key",
|
||||
"nodes[0].executor.version",
|
||||
"nodes[0].parameters",
|
||||
"nodes[0].inputs",
|
||||
])
|
||||
assert.ok(map[path], path);
|
||||
for (const [index, node] of ir.nodes.entries())
|
||||
for (const input of Object.keys(semanticCatalog[node.executor.key].inputs))
|
||||
assert.ok(map[`nodes[${index}].inputs.${input}`]);
|
||||
assert.ok(Object.keys(map).some((path) => /parameters\..+\[|parameters\..+\..+/.test(path)));
|
||||
assert.ok(
|
||||
Object.keys(map).some((path) =>
|
||||
/parameters\..+\[|parameters\..+\..+/.test(path),
|
||||
),
|
||||
);
|
||||
const socket = Object.values(map).find((source) => source.kind === "socket");
|
||||
const unconnected = Object.values(map).find((source) => source.unconnected === true);
|
||||
const unconnected = Object.values(map).find(
|
||||
(source) => source.unconnected === true,
|
||||
);
|
||||
const link = Object.values(map).find((source) => source.kind === "link");
|
||||
assert.ok(socket?.socketId && unconnected?.socketId);
|
||||
assert.equal(ir.nodes.find((node) => node.id === "copy").inputs.source, undefined);
|
||||
for (const field of ["linkId", "fromNodeId", "fromSocketId", "toNodeId", "toSocketId", "muted"])
|
||||
assert.equal(
|
||||
ir.nodes.find((node) => node.id === "frame_out").inputs.source,
|
||||
undefined,
|
||||
);
|
||||
for (const field of [
|
||||
"linkId",
|
||||
"fromNodeId",
|
||||
"fromSocketId",
|
||||
"toNodeId",
|
||||
"toSocketId",
|
||||
"muted",
|
||||
])
|
||||
assert.ok(Object.hasOwn(link, field), field);
|
||||
assert.ok(Object.isFrozen(map) && Object.isFrozen(link));
|
||||
});
|
||||
test("texture source maps identify every flat authored control", () => {
|
||||
const snapshot = fixture();
|
||||
const hdr = snapshot.nodes.find((node) => node.id === "hdr");
|
||||
hdr.parameters.viewFormat.value = "rgba16_float";
|
||||
const ir = adaptFxNodeSnapshot(snapshot);
|
||||
const index = ir.nodes.findIndex((node) => node.id === "hdr");
|
||||
const root = `nodes[${index}].parameters`;
|
||||
const map = getSourceMap(ir);
|
||||
const source = (parameter) => ({
|
||||
kind: "parameter",
|
||||
nodeId: "hdr",
|
||||
parameter,
|
||||
});
|
||||
assert.deepEqual(map[`${root}.residency`], source("residency"));
|
||||
assert.deepEqual(map[`${root}.texture`], { kind: "node", nodeId: "hdr" });
|
||||
for (const [path, parameter] of [
|
||||
["dimension", "dimension"],
|
||||
["format", "format"],
|
||||
["extent", "extentMode"],
|
||||
["extent.kind", "extentMode"],
|
||||
["extent.depthOrArrayLayers", "depthOrArrayLayers"],
|
||||
["extent.width", "extentMode"],
|
||||
["extent.width.numerator", "relativeWidthNumerator"],
|
||||
["extent.width.denominator", "relativeWidthDenominator"],
|
||||
["extent.height", "extentMode"],
|
||||
["extent.height.numerator", "relativeHeightNumerator"],
|
||||
["extent.height.denominator", "relativeHeightDenominator"],
|
||||
["mipLevelCount", "mipLevelCount"],
|
||||
["sampleCount", "sampleCount"],
|
||||
["viewFormats", "viewFormat"],
|
||||
["viewFormats[0]", "viewFormat"],
|
||||
])
|
||||
assert.deepEqual(map[`${root}.texture.${path}`], source(parameter), path);
|
||||
assert.ok(!Object.values(map).some((value) => value.parameter === "texture"));
|
||||
|
||||
const absolute = fixture();
|
||||
absolute.nodes.find((node) => node.id === "hdr").parameters.extentMode.value =
|
||||
"absolute";
|
||||
const absoluteIr = adaptFxNodeSnapshot(absolute);
|
||||
const absoluteIndex = absoluteIr.nodes.findIndex((node) => node.id === "hdr");
|
||||
const absoluteMap = getSourceMap(absoluteIr);
|
||||
assert.deepEqual(
|
||||
absoluteMap[`nodes[${absoluteIndex}].parameters.texture.extent.width`],
|
||||
source("absoluteWidth"),
|
||||
);
|
||||
assert.deepEqual(
|
||||
absoluteMap[`nodes[${absoluteIndex}].parameters.texture.extent.height`],
|
||||
source("absoluteHeight"),
|
||||
);
|
||||
});
|
||||
test("unsupported texture diagnostics map to their exact authored controls", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const index = ir.nodes.findIndex((node) => node.id === "hdr");
|
||||
for (const [suffix, parameter] of [
|
||||
["dimension", "dimension"],
|
||||
["mipLevelCount", "mipLevelCount"],
|
||||
["sampleCount", "sampleCount"],
|
||||
["extent.depthOrArrayLayers", "depthOrArrayLayers"],
|
||||
]) {
|
||||
const path = `nodes[${index}].parameters.texture.${suffix}`;
|
||||
const mapped = mapAuthoringDiagnostic(
|
||||
ir,
|
||||
new RendererError("GRAPH_UNSUPPORTED_FEATURE", {
|
||||
message: "unsupported",
|
||||
path,
|
||||
}),
|
||||
);
|
||||
assert.equal(mapped.path, path);
|
||||
assert.deepEqual(mapped.source, {
|
||||
kind: "parameter",
|
||||
nodeId: "hdr",
|
||||
parameter,
|
||||
});
|
||||
}
|
||||
});
|
||||
test("diagnostic mapper creates a frozen RendererError DTO with fallbacks and prefix matching", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const original = new RendererError("GRAPH_INPUT", { message: "bad", field: "nodes[0].executor.key.more", nested: { x: 1 } });
|
||||
const original = new RendererError("GRAPH_INPUT", {
|
||||
message: "bad",
|
||||
field: "nodes[0].executor.key.more",
|
||||
nested: { x: 1 },
|
||||
});
|
||||
const mapped = mapAuthoringDiagnostic(ir, original);
|
||||
assert.notStrictEqual(mapped, original);
|
||||
assert.equal(mapped.code, original.code);
|
||||
@@ -262,24 +533,44 @@ test("controller shares in-flight apply", async () => {
|
||||
await a;
|
||||
});
|
||||
test("controller retains mapped diagnostic while apply rejects the original and subscriptions agree", async () => {
|
||||
const original = new RendererError("GRAPH_BAD", { path: "nodes[0].id", message: "bad" });
|
||||
const original = new RendererError("GRAPH_BAD", {
|
||||
path: "nodes[0].id",
|
||||
message: "bad",
|
||||
});
|
||||
const states = [];
|
||||
const c = new AuthoringController({
|
||||
adapt: (snapshot, revision) => adaptFxNodeSnapshot(snapshot, revision),
|
||||
renderer: { compileGraph: async () => { throw original; }, switchCompiledGraph: async () => {}, dropCompiledGraph: async () => {} },
|
||||
renderer: {
|
||||
compileGraph: async () => {
|
||||
throw original;
|
||||
},
|
||||
switchCompiledGraph: async () => {},
|
||||
dropCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.subscribe((state) => states.push(state));
|
||||
c.markDirty(fixture());
|
||||
await assert.rejects(c.apply(), (error) => error === original);
|
||||
assert.notStrictEqual(states.at(-1).error, original);
|
||||
let subscribed;
|
||||
c.subscribe((state) => { subscribed = state; })();
|
||||
c.subscribe((state) => {
|
||||
subscribed = state;
|
||||
})();
|
||||
assert.strictEqual(subscribed.error, states.at(-1).error);
|
||||
await c.destroy();
|
||||
});
|
||||
test("apply after destroy does not compile and destroy returns one strict promise", async () => {
|
||||
let compiles = 0;
|
||||
const c = new AuthoringController({ adapt: () => ({}), renderer: { compileGraph: async () => { compiles++; }, dropCompiledGraph: async () => {}, switchCompiledGraph: async () => {} } });
|
||||
const c = new AuthoringController({
|
||||
adapt: () => ({}),
|
||||
renderer: {
|
||||
compileGraph: async () => {
|
||||
compiles++;
|
||||
},
|
||||
dropCompiledGraph: async () => {},
|
||||
switchCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.markDirty({});
|
||||
const first = c.destroy();
|
||||
assert.strictEqual(c.destroy(), first);
|
||||
|
||||
@@ -1,91 +1,295 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
culling,
|
||||
ember,
|
||||
hdr,
|
||||
midnight,
|
||||
renderGraphPresets,
|
||||
} from "../static/render-graph/presets.js";
|
||||
test("presets use canonical node graphs", () => {
|
||||
assert.deepEqual(Object.keys(renderGraphPresets), [
|
||||
"midnight",
|
||||
"ember",
|
||||
"hdr",
|
||||
"culling",
|
||||
"tone",
|
||||
"edges",
|
||||
"bloom",
|
||||
"combined",
|
||||
]);
|
||||
import * as presets from "../static/render-graph/presets.js";
|
||||
|
||||
const order = [
|
||||
"midnight",
|
||||
"ember",
|
||||
"hdr",
|
||||
"culling",
|
||||
"tone",
|
||||
"edges",
|
||||
"bloom",
|
||||
"combined",
|
||||
];
|
||||
const sequences = {
|
||||
midnight: [
|
||||
["ldr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
ember: [
|
||||
["ldr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
hdr: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
culling: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["cull", "frustum_cull"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
tone: [
|
||||
["ldr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["tone", "tone_map"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
edges: [
|
||||
["ldr", "texture"],
|
||||
["edge_hdr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["edges", "luminance_edge"],
|
||||
["tone", "tone_map"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
bloom: [
|
||||
["ldr", "texture"],
|
||||
["half_a", "texture"],
|
||||
["half_b", "texture"],
|
||||
["half_c", "texture"],
|
||||
["composite_hdr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["extract", "bloom_extract"],
|
||||
["blur_h", "bloom_blur"],
|
||||
["blur_v", "bloom_blur"],
|
||||
["composite", "bloom_composite"],
|
||||
["tone", "tone_map"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
combined: [
|
||||
["ldr", "texture"],
|
||||
["edge_hdr", "texture"],
|
||||
["half_a", "texture"],
|
||||
["half_b", "texture"],
|
||||
["half_c", "texture"],
|
||||
["composite_hdr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["extract", "bloom_extract"],
|
||||
["blur_h", "bloom_blur"],
|
||||
["blur_v", "bloom_blur"],
|
||||
["composite", "bloom_composite"],
|
||||
["edges", "luminance_edge"],
|
||||
["tone", "tone_map"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
};
|
||||
|
||||
test("presets have the exact canonical pipeline identities, schemas, and node sequences", () => {
|
||||
assert.deepEqual(Object.keys(presets.renderGraphPresets), order);
|
||||
assert.deepEqual(
|
||||
[midnight.graphId, ember.graphId],
|
||||
["preset_midnight", "preset_ember"],
|
||||
);
|
||||
assert.notDeepEqual(midnight.nodes[6].parameters.clearColor, ember.nodes[6].parameters.clearColor);
|
||||
for (const graph of [midnight, ember]) {
|
||||
assert.equal(graph.schemaVersion, 2);
|
||||
assert.equal(graph.revision, 1);
|
||||
assert.equal(graph.nodes[6].executor.key, "legacy_forward");
|
||||
assert.deepEqual(graph.nodes[6].inputs.colorTarget, { node: "surface", socket: "surface" });
|
||||
assert.equal(graph.nodes.at(-1).executor.key, "present");
|
||||
}
|
||||
assert.equal(hdr.schemaVersion, 2);
|
||||
assert.equal(hdr.revision, 1);
|
||||
assert.equal(hdr.graphId, "preset_hdr_fullscreen");
|
||||
assert.equal(
|
||||
new Set(Object.values(renderGraphPresets).map((graph) => graph.graphId))
|
||||
.size,
|
||||
8,
|
||||
);
|
||||
assert.deepEqual(
|
||||
hdr.nodes.map((node) => [node.id, node.executor.key]),
|
||||
order.map((name) => presets[name].graphId),
|
||||
[
|
||||
["surface", "surface_target"],
|
||||
["hdr", "texture_spec"],
|
||||
["depth", "texture_spec"],
|
||||
["scene", "scene_table"],
|
||||
["visible", "visibility_flags"],
|
||||
["query", "mesh_query"],
|
||||
["depth_config", "depth_stencil_config"],
|
||||
["forward", "legacy_forward"],
|
||||
["copy", "fullscreen_copy"],
|
||||
["present", "present"],
|
||||
"preset_midnight",
|
||||
"preset_ember",
|
||||
"preset_hdr_fullscreen",
|
||||
"preset_gpu_culling",
|
||||
"preset_tone",
|
||||
"preset_edges",
|
||||
"preset_bloom",
|
||||
"preset_combined",
|
||||
],
|
||||
);
|
||||
const byId = Object.fromEntries(hdr.nodes.map((node) => [node.id, node]));
|
||||
assert.equal(byId.hdr.parameters.texture.format, "rgba16_float");
|
||||
assert.deepEqual(byId.query.inputs, {
|
||||
scene: { node: "scene", socket: "scene" },
|
||||
isVisible: { node: "visible", socket: "flags" },
|
||||
for (const name of order) {
|
||||
const graph = presets[name];
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [2, 1]);
|
||||
assert.equal(
|
||||
new Set(graph.nodes.map((node) => node.id)).size,
|
||||
graph.nodes.length,
|
||||
);
|
||||
assert.deepEqual(
|
||||
graph.nodes.map((node) => [node.id, node.executor.key]),
|
||||
sequences[name],
|
||||
);
|
||||
assert.equal(
|
||||
graph.nodes.filter((node) => node.executor.key === "frame_out").length,
|
||||
1,
|
||||
);
|
||||
assert.ok(
|
||||
graph.nodes.every(
|
||||
(node) => !["surface_target", "present"].includes(node.executor.key),
|
||||
),
|
||||
);
|
||||
assert.ok(!graph.nodes.some((node) => node.id === "copy"));
|
||||
}
|
||||
});
|
||||
|
||||
test("presets preserve common mesh, texture, query, pipeline, culling and post wiring", () => {
|
||||
const removed = [
|
||||
"texture_spec",
|
||||
"scene_table",
|
||||
"local_aabb_buffer",
|
||||
"camera_frustum",
|
||||
"visibility_flags",
|
||||
];
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.query.parameters, {
|
||||
visiblePredicate: "required_true",
|
||||
visibleDefault: true,
|
||||
frustumCulledPredicate: name === "culling" ? "required_false" : "any",
|
||||
frustumCulledDefault: false,
|
||||
});
|
||||
assert.deepEqual(byId.query.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||
assert.deepEqual(byId.query.inputs.isVisible, {
|
||||
node: "mesh",
|
||||
socket: "isVisible",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.mesh, {
|
||||
node: "mesh",
|
||||
socket: "mesh",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.draws, {
|
||||
node: "query",
|
||||
socket: "draws",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.depthTarget, {
|
||||
node: "depth",
|
||||
socket: "texture",
|
||||
});
|
||||
assert.equal(
|
||||
graph.nodes.filter((node) => node.executor.key === "pipeline_registry")
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
const pipelines = graph.nodes.filter(
|
||||
(node) => node.executor.key === "pipeline",
|
||||
);
|
||||
assert.deepEqual(
|
||||
pipelines.map((node) => node.parameters.pipeline),
|
||||
["ground_plane", "gltf_standard", "gltf_standard_double_sided"],
|
||||
);
|
||||
for (const pipeline of pipelines)
|
||||
assert.deepEqual(pipeline.inputs.activation, {
|
||||
node: "registry",
|
||||
socket: "activation",
|
||||
});
|
||||
assert.deepEqual(byId.pbr.inputs.colorTarget, {
|
||||
node: "ground",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.pbr.inputs.depthTarget, {
|
||||
node: "ground",
|
||||
socket: "depth",
|
||||
});
|
||||
assert.deepEqual(byId.pbr_double.inputs.colorTarget, {
|
||||
node: "pbr",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.pbr_double.inputs.depthTarget, {
|
||||
node: "pbr",
|
||||
socket: "depth",
|
||||
});
|
||||
assert.ok(
|
||||
graph.nodes
|
||||
.filter((node) => node.executor.key === "texture")
|
||||
.every((node) => node.parameters.texture.dimension === "d2"),
|
||||
);
|
||||
assert.ok(
|
||||
graph.nodes.every((node) => !removed.includes(node.executor.key)),
|
||||
);
|
||||
}
|
||||
const cull = Object.fromEntries(
|
||||
presets.culling.nodes.map((node) => [node.id, node]),
|
||||
);
|
||||
assert.deepEqual(cull.cull.parameters, { camera: "active" });
|
||||
assert.deepEqual(cull.cull.inputs, {
|
||||
mesh: { node: "mesh", socket: "mesh" },
|
||||
localAabbs: { node: "mesh", socket: "localAabbs" },
|
||||
});
|
||||
assert.deepEqual(byId.query.parameters.filters, [
|
||||
{ flag: "isVisible", predicate: "required_true" },
|
||||
{ flag: "isFrustumCulled", predicate: "any" },
|
||||
]);
|
||||
assert.deepEqual(byId.forward.inputs.colorTarget, {
|
||||
node: "hdr",
|
||||
socket: "spec",
|
||||
assert.deepEqual(cull.query.inputs.isFrustumCulled, {
|
||||
node: "cull",
|
||||
socket: "isFrustumCulled",
|
||||
});
|
||||
assert.deepEqual(byId.copy.executor, { key: "fullscreen_copy", version: 1 });
|
||||
assert.deepEqual(byId.copy.inputs, {
|
||||
source: { node: "forward", socket: "color" },
|
||||
colorTarget: { node: "surface", socket: "surface" },
|
||||
});
|
||||
assert.deepEqual(byId.present.inputs.surface, {
|
||||
node: "copy",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(
|
||||
culling.nodes
|
||||
.filter((node) => ["frustum_cull", "mesh_query"].includes(node.executor.key))
|
||||
.map((node) => node.executor.key),
|
||||
["frustum_cull", "mesh_query"],
|
||||
for (const name of ["tone", "edges", "bloom", "combined"])
|
||||
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
||||
const finalSource = {
|
||||
hdr: "pbr_double",
|
||||
culling: "pbr_double",
|
||||
tone: "tone",
|
||||
edges: "tone",
|
||||
bloom: "tone",
|
||||
combined: "tone",
|
||||
midnight: "pbr_double",
|
||||
ember: "pbr_double",
|
||||
};
|
||||
for (const name of order)
|
||||
assert.deepEqual(presets[name].nodes.at(-1).inputs.color, {
|
||||
node: finalSource[name],
|
||||
socket: "color",
|
||||
});
|
||||
assert.equal(
|
||||
presets.tone.nodes.find((node) => node.id === "tone").inputs.source.node,
|
||||
"pbr_double",
|
||||
);
|
||||
assert.equal(
|
||||
presets.edges.nodes.find((node) => node.id === "tone").inputs.source.node,
|
||||
"edges",
|
||||
);
|
||||
assert.equal(
|
||||
presets.bloom.nodes.find((node) => node.id === "tone").inputs.source.node,
|
||||
"composite",
|
||||
);
|
||||
assert.equal(
|
||||
presets.combined.nodes.find((node) => node.id === "tone").inputs.source
|
||||
.node,
|
||||
"edges",
|
||||
);
|
||||
const cullingQuery = culling.nodes.find((node) => node.id === "query");
|
||||
assert.equal(cullingQuery.parameters.filters[1].predicate, "required_false");
|
||||
assert.deepEqual(cullingQuery.inputs.isFrustumCulled, {
|
||||
node: "cull",
|
||||
socket: "flags",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user