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,
|
texture: TextureDescriptor,
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
#[serde(deny_unknown_fields)]
|
||||||
struct DepthParameters {
|
struct CullParameters {
|
||||||
depth_compare: CompareFunction,
|
camera: ActiveCamera,
|
||||||
depth_write_enabled: bool,
|
|
||||||
clear_depth: f32,
|
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
#[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],
|
clear_color: [f64; 4],
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[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 {
|
match predicate {
|
||||||
TriStatePredicate::Any => true,
|
RuntimePredicate::Any => true,
|
||||||
TriStatePredicate::RequiredTrue => flag,
|
RuntimePredicate::RequiredTrue => flag,
|
||||||
TriStatePredicate::RequiredFalse => !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() {
|
Ok(match node.executor.key.as_str() {
|
||||||
"surface_target" => empty!(NormalizedParameters::SurfaceTarget),
|
"mesh" => empty!(NormalizedParameters::Mesh),
|
||||||
"scene_table" => empty!(NormalizedParameters::SceneTable),
|
"frustum_cull" => {
|
||||||
"local_aabb_buffer" => empty!(NormalizedParameters::LocalAabbBuffer),
|
let p: CullParameters =
|
||||||
"camera_frustum" => empty!(NormalizedParameters::CameraFrustum),
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
"visibility_flags" => empty!(NormalizedParameters::VisibilityFlags),
|
NormalizedParameters::FrustumCull { camera: p.camera }
|
||||||
"frustum_cull" => empty!(NormalizedParameters::FrustumCull),
|
}
|
||||||
"fullscreen_copy" => empty!(NormalizedParameters::FullscreenCopy),
|
"fullscreen_copy" => empty!(NormalizedParameters::FullscreenCopy),
|
||||||
"tone_map" => {
|
"tone_map" => {
|
||||||
let p: ToneMapParameters =
|
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"))?,
|
strength: range(p.strength, 0.0, 16.0, format!("{base}.strength"))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"present" => empty!(NormalizedParameters::Present),
|
"frame_out" => empty!(NormalizedParameters::FrameOut),
|
||||||
"texture_spec" => {
|
"texture" => {
|
||||||
let p: TextureParameters =
|
let p: TextureParameters =
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -416,100 +427,86 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
format!("{base}.residency"),
|
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,
|
residency: p.residency,
|
||||||
texture: normalize_texture(p.texture, &base)?,
|
descriptor: normalize_texture(p.texture, &base)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"mesh_query" => {
|
"mesh_query" => {
|
||||||
let object = node.parameters.as_object().ok_or_else(|| {
|
let p: QueryParameters =
|
||||||
error(
|
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||||
"GRAPH_PARAMETERS_INVALID",
|
let fold = |predicate, default, linked| match (predicate, linked, default) {
|
||||||
"parameters must be an object",
|
(TriStatePredicate::Any, _, _) => RuntimePredicate::Any,
|
||||||
base.clone(),
|
(TriStatePredicate::RequiredTrue, true, _) => RuntimePredicate::RequiredTrue,
|
||||||
)
|
(TriStatePredicate::RequiredFalse, true, _) => RuntimePredicate::RequiredFalse,
|
||||||
})?;
|
(TriStatePredicate::RequiredTrue, false, true)
|
||||||
if object.len() != 1 || !object.contains_key("filters") {
|
| (TriStatePredicate::RequiredFalse, false, false) => RuntimePredicate::Any,
|
||||||
return Err(error(
|
_ => RuntimePredicate::Never,
|
||||||
"GRAPH_PARAMETERS_INVALID",
|
};
|
||||||
"mesh query parameters must contain only filters",
|
let mut visible = fold(
|
||||||
base.clone(),
|
p.visible_predicate,
|
||||||
));
|
p.visible_default,
|
||||||
}
|
node.inputs.contains_key("isVisible"),
|
||||||
let filters = object["filters"].as_array().ok_or_else(|| {
|
);
|
||||||
error(
|
let mut culled = fold(
|
||||||
"GRAPH_PARAMETERS_INVALID",
|
p.frustum_culled_predicate,
|
||||||
"filters must be an array",
|
p.frustum_culled_default,
|
||||||
format!("{base}.filters"),
|
node.inputs.contains_key("isFrustumCulled"),
|
||||||
)
|
);
|
||||||
})?;
|
if visible == RuntimePredicate::Never || culled == RuntimePredicate::Never {
|
||||||
let mut found = [None, None];
|
visible = RuntimePredicate::Never;
|
||||||
for (j, value) in filters.iter().enumerate() {
|
culled = RuntimePredicate::Never;
|
||||||
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"),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
NormalizedParameters::MeshQuery {
|
NormalizedParameters::MeshQuery {
|
||||||
filters: [
|
visible_predicate: visible,
|
||||||
NormalizedMeshFilter {
|
frustum_culled_predicate: culled,
|
||||||
flag: MeshFlag::IsVisible,
|
|
||||||
predicate: found[0].unwrap(),
|
|
||||||
},
|
|
||||||
NormalizedMeshFilter {
|
|
||||||
flag: MeshFlag::IsFrustumCulled,
|
|
||||||
predicate: found[1].unwrap(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"depth_stencil_config" => {
|
"pipeline_registry" => empty!(NormalizedParameters::PipelineRegistry),
|
||||||
let p: DepthParameters =
|
"pipeline" => {
|
||||||
|
let p: PipelineParameters =
|
||||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
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) {
|
if !p.clear_depth.is_finite() || !(0.0..=1.0).contains(&p.clear_depth) {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_PARAMETERS_INVALID",
|
"GRAPH_PARAMETERS_INVALID",
|
||||||
@@ -517,17 +514,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
format!("{base}.clearDepth"),
|
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()) {
|
if p.clear_color.iter().any(|x| !x.is_finite()) {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_PARAMETERS_INVALID",
|
"GRAPH_PARAMETERS_INVALID",
|
||||||
@@ -535,7 +521,11 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
|||||||
format!("{base}.clearColor"),
|
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,
|
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 {
|
if graph.schema_version != 2 {
|
||||||
return Err(GraphError::new(
|
return Err(GraphError::new(
|
||||||
"GRAPH_SCHEMA_UNSUPPORTED",
|
"GRAPH_SCHEMA_UNSUPPORTED",
|
||||||
@@ -673,8 +650,21 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, n)| decode(n, i))
|
.map(|(i, n)| decode(n, i))
|
||||||
.collect::<Result<_, _>>()?;
|
.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() {
|
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(
|
return Err(error(
|
||||||
"GRAPH_NODE_STATE_INVALID",
|
"GRAPH_NODE_STATE_INVALID",
|
||||||
"muted nodes are unsupported",
|
"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 (i, n) in graph.nodes.iter().enumerate() {
|
||||||
for input in contracts[i].inputs {
|
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 !n.inputs.contains_key(input.name) {
|
||||||
if input.cardinality == InputCardinality::RequiredOne
|
if input.cardinality == InputCardinality::RequiredOne
|
||||||
|| (!inactive
|
|| (!inactive
|
||||||
&& matches!(params[i], NormalizedParameters::MeshQuery { .. })
|
&& matches!(params[i], NormalizedParameters::MeshQuery { .. })
|
||||||
&& input.name != "scene")
|
&& input.name != "mesh")
|
||||||
{
|
{
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_SOCKET_CARDINALITY",
|
"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()];
|
let mut bound: Vec<BTreeMap<&str, BoundInput>> = vec![BTreeMap::new(); graph.nodes.len()];
|
||||||
for (i, n) in graph.nodes.iter().enumerate() {
|
for (i, n) in graph.nodes.iter().enumerate() {
|
||||||
for input in contracts[i].inputs {
|
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 {
|
let Some(r) = n.inputs.get(input.name) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -741,10 +731,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, o)| o.name == r.socket)
|
.find(|(_, o)| o.name == r.socket)
|
||||||
.expect("producer sockets were globally validated");
|
.expect("producer sockets were globally validated");
|
||||||
let attachment_shape_checked_later = contracts[i].key == "legacy_forward"
|
if !accepts(input.accepted, out.semantic_type) {
|
||||||
&& input.name == "depthTarget"
|
|
||||||
&& out.semantic_type == SemanticType::SurfaceTarget;
|
|
||||||
if !accepts(input.accepted, out.semantic_type) && !attachment_shape_checked_later {
|
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||||
"socket type mismatch",
|
"socket type mismatch",
|
||||||
@@ -782,15 +769,26 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if !seen.insert(k.0) {
|
if !seen.insert(k.0) {
|
||||||
return None;
|
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);
|
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() {
|
for (i, c) in contracts.iter().enumerate() {
|
||||||
if c.key == "frustum_cull"
|
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)
|
!= root(bound[i]["localAabbs"].producer, &bound, &contracts)
|
||||||
{
|
{
|
||||||
return Err(error(
|
return Err(error(
|
||||||
@@ -799,11 +797,23 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
format!("nodes[{i}].inputs.localAabbs"),
|
format!("nodes[{i}].inputs.localAabbs"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if matches!(c.key, "mesh_query" | "legacy_forward") {
|
if matches!(c.key, "mesh_query" | "pipeline_registry" | "pipeline") {
|
||||||
let scene = root(bound[i]["scene"].producer, &bound, &contracts);
|
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] {
|
for (s, b) in &bound[i] {
|
||||||
if b.active
|
if b.active
|
||||||
&& matches!(*s, "isVisible" | "isFrustumCulled" | "draws")
|
&& matches!(
|
||||||
|
*s,
|
||||||
|
"isVisible"
|
||||||
|
| "isFrustumCulled"
|
||||||
|
| "draws"
|
||||||
|
| "pipelineIndices"
|
||||||
|
| "activation"
|
||||||
|
)
|
||||||
&& root(b.producer, &bound, &contracts) != scene
|
&& root(b.producer, &bound, &contracts) != scene
|
||||||
{
|
{
|
||||||
return Err(error(
|
return Err(error(
|
||||||
@@ -853,7 +863,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
let mut stack: Vec<_> = contracts
|
let mut stack: Vec<_> = contracts
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, c)| c.inherently_observable)
|
.filter(|(i, c)| c.inherently_observable && graph.nodes[*i].state == NodeState::Enabled)
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.collect();
|
.collect();
|
||||||
while let Some(i) = stack.pop() {
|
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.
|
// 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 output_ids = BTreeMap::new();
|
||||||
let mut resource_meta = Vec::new();
|
let mut resource_meta = Vec::new();
|
||||||
for i in 0..graph.nodes.len() {
|
for i in 0..graph.nodes.len() {
|
||||||
if live.contains(&i) {
|
if live.contains(&i) {
|
||||||
for (o, out) in contracts[i].outputs.iter().enumerate() {
|
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;
|
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));
|
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();
|
let source = output_ids.get(&OutputKey(i, 0)).copied();
|
||||||
match ¶ms[i] {
|
match ¶ms[i] {
|
||||||
NormalizedParameters::SurfaceTarget => {
|
NormalizedParameters::Texture {
|
||||||
let id = families.len() as u32;
|
residency,
|
||||||
let r = source.unwrap();
|
descriptor,
|
||||||
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 } => {
|
|
||||||
let id = families.len() as u32;
|
let id = families.len() as u32;
|
||||||
let r = source.unwrap();
|
let r = source.unwrap();
|
||||||
source_family.insert(OutputKey(i, 0), id);
|
source_family.insert(OutputKey(i, 0), id);
|
||||||
@@ -918,7 +923,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
source: TextureFamilySource::AuthoredTexture {
|
source: TextureFamilySource::AuthoredTexture {
|
||||||
resource: r,
|
resource: r,
|
||||||
residency: *residency,
|
residency: *residency,
|
||||||
descriptor: texture.clone(),
|
descriptor: descriptor.clone(),
|
||||||
},
|
},
|
||||||
lifetime: Lifetime {
|
lifetime: Lifetime {
|
||||||
first_use: 0,
|
first_use: 0,
|
||||||
@@ -940,7 +945,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let transition_sockets: &[(&str, u16)] = match contracts[i].key {
|
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"
|
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur" | "bloom_composite"
|
||||||
| "luminance_edge" => &[("colorTarget", 0)],
|
| "luminance_edge" => &[("colorTarget", 0)],
|
||||||
_ => continue,
|
_ => continue,
|
||||||
@@ -1066,7 +1071,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
for input in contract
|
for input in contract
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|input| matches!(input.role, InputRole::Present | InputRole::SampledTexture))
|
.filter(|input| matches!(input.role, InputRole::SampledTexture))
|
||||||
{
|
{
|
||||||
let key = bound[i][input.name].producer;
|
let key = bound[i][input.name].producer;
|
||||||
if !version_of.contains_key(&key) {
|
if !version_of.contains_key(&key) {
|
||||||
@@ -1103,7 +1108,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
if !live.contains(&i) {
|
if !live.contains(&i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if contracts[i].key == "legacy_forward" {
|
if contracts[i].key == "pipeline" {
|
||||||
if bound[i]["colorTarget"].producer == bound[i]["depthTarget"].producer
|
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)
|
|| matches!((version_of.get(&OutputKey(i, 0)), version_of.get(&OutputKey(i, 1))), (Some((cf, _, _)), Some((df, _, _))) if cf == df)
|
||||||
{
|
{
|
||||||
@@ -1113,7 +1118,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
format!("nodes[{i}].inputs"),
|
format!("nodes[{i}].inputs"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if contracts[i]
|
} else if contracts[i].key != "frame_out"
|
||||||
|
&& contracts[i]
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|input| matches!(input.role, InputRole::SampledTexture))
|
.any(|input| matches!(input.role, InputRole::SampledTexture))
|
||||||
@@ -1176,7 +1182,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
|
|
||||||
// Validate every independently resolved attachment before graph cycle reporting.
|
// Validate every independently resolved attachment before graph cycle reporting.
|
||||||
for i in 0..graph.nodes.len() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let (Some(&(cf, _, _)), Some(&(df, _, _))) = (
|
let (Some(&(cf, _, _)), Some(&(df, _, _))) = (
|
||||||
@@ -1185,33 +1191,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let cd = match &families[cf as usize].source {
|
let TextureFamilySource::AuthoredTexture { descriptor: cd, .. } =
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
&families[cf as usize].source;
|
||||||
_ => None,
|
let TextureFamilySource::AuthoredTexture { descriptor: dd, .. } =
|
||||||
};
|
&families[df as usize].source;
|
||||||
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 ok_depth = dd.dimension == TextureDimension::D2
|
let ok_depth = dd.dimension == TextureDimension::D2
|
||||||
&& dd.format == TextureFormat::Depth32Float
|
&& dd.format == TextureFormat::Depth32Float
|
||||||
&& dd.sample_count == 1
|
&& dd.sample_count == 1
|
||||||
&& extent_layers(&dd.extent) == 1;
|
&& extent_layers(&dd.extent) == 1;
|
||||||
let ok_color = cd.is_none_or(|d| {
|
let ok_color = cd.format != TextureFormat::Depth32Float
|
||||||
d.format != TextureFormat::Depth32Float
|
&& cd.dimension == dd.dimension
|
||||||
&& d.dimension == dd.dimension
|
&& cd.extent == dd.extent
|
||||||
&& d.extent == dd.extent
|
&& cd.sample_count == 1;
|
||||||
&& d.sample_count == 1
|
if !ok_depth || !ok_color {
|
||||||
});
|
|
||||||
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 {
|
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_ILLEGAL_ACCESS",
|
"GRAPH_ILLEGAL_ACCESS",
|
||||||
"attachments are incompatible",
|
"attachments are incompatible",
|
||||||
@@ -1222,6 +1214,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
|
|
||||||
for i in 0..graph.nodes.len() {
|
for i in 0..graph.nodes.len() {
|
||||||
if !live.contains(&i)
|
if !live.contains(&i)
|
||||||
|
|| contracts[i].key == "frame_out"
|
||||||
|| !contracts[i]
|
|| !contracts[i]
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1242,19 +1235,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
};
|
};
|
||||||
let source_descriptor = match &families[source_family_id as usize].source {
|
let source_descriptor = match &families[source_family_id as usize].source {
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => descriptor,
|
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
|
let source_ok = source_descriptor.format == TextureFormat::Rgba16Float
|
||||||
&& is_single_view_d2(source_descriptor);
|
&& is_single_view_d2(source_descriptor);
|
||||||
let target_descriptor = match &families[target_family_id as usize].source {
|
let target_descriptor = match &families[target_family_id as usize].source {
|
||||||
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
TextureFamilySource::AuthoredTexture { descriptor, .. } => Some(descriptor),
|
||||||
TextureFamilySource::ImportedSurface { .. } => None,
|
|
||||||
};
|
};
|
||||||
let authored_target_ok = target_descriptor.is_some_and(|descriptor| {
|
let authored_target_ok = target_descriptor.is_some_and(|descriptor| {
|
||||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(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, .. } => {
|
TextureFamilySource::AuthoredTexture { descriptor, .. } => {
|
||||||
descriptor.format == TextureFormat::Rgba16Float && is_single_view_d2(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 {
|
} else {
|
||||||
true
|
true
|
||||||
@@ -1295,7 +1273,12 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
&& descriptor.extent == source_descriptor.extent
|
&& 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_extract" => authored_target_ok,
|
||||||
"bloom_blur" | "luminance_edge" => authored_target_ok && target_matches_source,
|
"bloom_blur" | "luminance_edge" => authored_target_ok && target_matches_source,
|
||||||
"bloom_composite" => authored_target_ok && target_matches_source && bloom_input_ok,
|
"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() {
|
for (i, contract) in contracts.iter().enumerate() {
|
||||||
if !live.contains(&i) || contract.key != "present" {
|
if !live.contains(&i) || contract.key != "frame_out" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let key = bound[i]["surface"].producer;
|
let key = bound[i]["color"].producer;
|
||||||
let Some(&(family, _, _)) = version_of.get(&key) else {
|
let Some(&(family, _, _)) = version_of.get(&key) else {
|
||||||
if !matches!(resolved.get(&key), Some(ResolvedTransition::Cyclic)) {
|
if !matches!(resolved.get(&key), Some(ResolvedTransition::Cyclic)) {
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_UNINITIALIZED_RESOURCE",
|
"GRAPH_UNINITIALIZED_RESOURCE",
|
||||||
"present source is not produced",
|
"frame output source is not produced",
|
||||||
format!("nodes[{i}].inputs.surface"),
|
format!("nodes[{i}].inputs.color"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !matches!(
|
let TextureFamilySource::AuthoredTexture { descriptor, .. } =
|
||||||
families[family as usize].source,
|
&families[family as usize].source;
|
||||||
TextureFamilySource::ImportedSurface { .. }
|
if !is_filterable_frame_color(descriptor) {
|
||||||
) {
|
|
||||||
return Err(error(
|
return Err(error(
|
||||||
"GRAPH_ILLEGAL_ACCESS",
|
"GRAPH_ILLEGAL_ACCESS",
|
||||||
"offscreen textures cannot be presented",
|
"frame output requires a filterable single-view d2 color texture",
|
||||||
format!("nodes[{i}].inputs.surface"),
|
format!("nodes[{i}].inputs.color"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1463,17 +1445,18 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
for (i, o, out) in resource_meta {
|
for (i, o, out) in resource_meta {
|
||||||
let key = OutputKey(i, o);
|
let key = OutputKey(i, o);
|
||||||
let id = output_ids[&key];
|
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 {
|
let plan = match out.semantic_type {
|
||||||
SemanticType::SurfaceTarget => ResourcePlan::SurfaceTarget {
|
SemanticType::Texture if matches!(params[i], NormalizedParameters::Texture { .. }) => {
|
||||||
family: source_family[&key],
|
if let NormalizedParameters::Texture {
|
||||||
},
|
residency,
|
||||||
SemanticType::TextureSpec => {
|
descriptor,
|
||||||
if let NormalizedParameters::TextureSpec { residency, texture } = ¶ms[i] {
|
} = ¶ms[i]
|
||||||
ResourcePlan::TextureSpec {
|
{
|
||||||
|
ResourcePlan::TextureSource {
|
||||||
family: source_family[&key],
|
family: source_family[&key],
|
||||||
residency: *residency,
|
residency: *residency,
|
||||||
descriptor: texture.clone(),
|
descriptor: descriptor.clone(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
@@ -1490,27 +1473,20 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
allocation: None,
|
allocation: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SemanticType::SceneTable => ResourcePlan::SceneTable,
|
SemanticType::MeshData => ResourcePlan::MeshData,
|
||||||
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { scene: scene() },
|
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { mesh: mesh() },
|
||||||
SemanticType::CameraFrustum => ResourcePlan::CameraFrustum,
|
|
||||||
SemanticType::BooleanFlagBuffer => {
|
SemanticType::BooleanFlagBuffer => {
|
||||||
if let OutputMetadata::BooleanFlag { flag } = out.metadata {
|
if let OutputMetadata::BooleanFlag { flag } = out.metadata {
|
||||||
ResourcePlan::BooleanFlagBuffer {
|
ResourcePlan::BooleanFlagBuffer { mesh: mesh(), flag }
|
||||||
scene: scene(),
|
|
||||||
flag,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SemanticType::DrawStream => ResourcePlan::DrawStream { scene: scene() },
|
|
||||||
SemanticType::DepthStencilConfig => {
|
|
||||||
if let NormalizedParameters::DepthStencilConfig { config } = ¶ms[i] {
|
|
||||||
ResourcePlan::DepthStencilConfig { config: *config }
|
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
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 {
|
resources.push(CompiledResource {
|
||||||
original_node_index: i as u32,
|
original_node_index: i as u32,
|
||||||
@@ -1557,9 +1533,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
let kind = match contracts[i].key {
|
let kind = match contracts[i].key {
|
||||||
"frustum_cull" => {
|
"frustum_cull" => {
|
||||||
for (s, m) in [
|
for (s, m) in [
|
||||||
("scene", AccessMode::StorageRead),
|
("mesh", AccessMode::StorageRead),
|
||||||
("localAabbs", AccessMode::StorageRead),
|
("localAabbs", AccessMode::StorageRead),
|
||||||
("frustum", AccessMode::UniformRead),
|
|
||||||
] {
|
] {
|
||||||
accesses.push(CompiledAccess {
|
accesses.push(CompiledAccess {
|
||||||
socket: s.into(),
|
socket: s.into(),
|
||||||
@@ -1569,7 +1544,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
let r = output_ids[&OutputKey(i, 0)];
|
let r = output_ids[&OutputKey(i, 0)];
|
||||||
accesses.push(CompiledAccess {
|
accesses.push(CompiledAccess {
|
||||||
socket: "flags".into(),
|
socket: "isFrustumCulled".into(),
|
||||||
resource: r,
|
resource: r,
|
||||||
mode: AccessMode::StorageWrite {
|
mode: AccessMode::StorageWrite {
|
||||||
full_overwrite: true,
|
full_overwrite: true,
|
||||||
@@ -1580,7 +1555,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"mesh_query" => {
|
"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) {
|
if let Some(b) = bound[i].get(s).filter(|b| b.active) {
|
||||||
accesses.push(CompiledAccess {
|
accesses.push(CompiledAccess {
|
||||||
socket: s.into(),
|
socket: s.into(),
|
||||||
@@ -1600,23 +1575,38 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
work: ComputeWork::MeshQuery,
|
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 color = output_ids[&OutputKey(i, 0)];
|
||||||
let depth = output_ids[&OutputKey(i, 1)];
|
let depth = output_ids[&OutputKey(i, 1)];
|
||||||
let clear = match params[i] {
|
let clear = match params[i] {
|
||||||
NormalizedParameters::LegacyForward { clear_color } => clear_color,
|
NormalizedParameters::Pipeline { clear_color, .. } => clear_color,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let config_node = bound[i]["depthStencil"].producer.0;
|
let clear_depth = match ¶ms[i] {
|
||||||
let dc = match params[config_node] {
|
NormalizedParameters::Pipeline { clear_depth, .. } => *clear_depth,
|
||||||
NormalizedParameters::DepthStencilConfig { config } => config,
|
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let cl = NormalizedColorLoad::Clear { value: clear };
|
let first_color = version_of[&OutputKey(i, 0)].1 == 0;
|
||||||
let dl = NormalizedDepthLoad::Clear {
|
let first_depth = version_of[&OutputKey(i, 1)].1 == 0;
|
||||||
value: dc.clear_depth,
|
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 {
|
accesses.push(CompiledAccess {
|
||||||
socket: s.into(),
|
socket: s.into(),
|
||||||
resource: input_resource(s),
|
resource: input_resource(s),
|
||||||
@@ -1634,7 +1624,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
location: 0,
|
location: 0,
|
||||||
load: cl,
|
load: cl,
|
||||||
store: StoreOp::Store,
|
store: StoreOp::Store,
|
||||||
full_overwrite: true,
|
full_overwrite: first_color,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
accesses.push(CompiledAccess {
|
accesses.push(CompiledAccess {
|
||||||
@@ -1643,7 +1633,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
mode: AccessMode::DepthAttachment {
|
mode: AccessMode::DepthAttachment {
|
||||||
load: dl,
|
load: dl,
|
||||||
store: StoreOp::Store,
|
store: StoreOp::Store,
|
||||||
full_overwrite: true,
|
full_overwrite: first_depth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ExecutionKind::Render {
|
ExecutionKind::Render {
|
||||||
@@ -1698,14 +1688,14 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
|||||||
depth_stencil: None,
|
depth_stencil: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"present" => {
|
"frame_out" => {
|
||||||
let r = input_resource("surface");
|
let r = input_resource("color");
|
||||||
accesses.push(CompiledAccess {
|
accesses.push(CompiledAccess {
|
||||||
socket: "surface".into(),
|
socket: "color".into(),
|
||||||
resource: r,
|
resource: r,
|
||||||
mode: AccessMode::Present,
|
mode: AccessMode::SampledTexture,
|
||||||
});
|
});
|
||||||
ExecutionKind::Present { surface: r }
|
ExecutionKind::FrameOut { color: r }
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
@@ -1803,13 +1793,27 @@ fn extent_layers(e: &NormalizedTextureExtent) -> u32 {
|
|||||||
} => *depth_or_array_layers,
|
} => *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.dimension == TextureDimension::D2
|
||||||
&& descriptor.sample_count == 1
|
&& descriptor.sample_count == 1
|
||||||
&& descriptor.mip_level_count == 1
|
&& descriptor.mip_level_count == 1
|
||||||
&& extent_layers(&descriptor.extent) == 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 rs: HashSet<_> = f.versions.iter().map(|v| v.resource).collect();
|
||||||
let mut u = BTreeSet::new();
|
let mut u = BTreeSet::new();
|
||||||
for e in executions {
|
for e in executions {
|
||||||
@@ -1842,7 +1846,7 @@ fn allocate(
|
|||||||
) -> (Vec<AllocationClass>, u32) {
|
) -> (Vec<AllocationClass>, u32) {
|
||||||
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = BTreeMap::new();
|
let mut grouped: BTreeMap<TextureCompatibilityKey, Vec<usize>> = BTreeMap::new();
|
||||||
for (i, f) in families.iter().enumerate() {
|
for (i, f) in families.iter().enumerate() {
|
||||||
if let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source {
|
let TextureFamilySource::AuthoredTexture { descriptor, .. } = &f.source;
|
||||||
grouped
|
grouped
|
||||||
.entry(TextureCompatibilityKey {
|
.entry(TextureCompatibilityKey {
|
||||||
dimension: descriptor.dimension,
|
dimension: descriptor.dimension,
|
||||||
@@ -1855,7 +1859,6 @@ fn allocate(
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(i);
|
.push(i);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut classes = Vec::new();
|
let mut classes = Vec::new();
|
||||||
let mut transient = 0;
|
let mut transient = 0;
|
||||||
for (key, ids) in grouped {
|
for (key, ids) in grouped {
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ use super::MeshFlag;
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum SemanticType {
|
pub enum SemanticType {
|
||||||
SurfaceTarget,
|
MeshData,
|
||||||
TextureSpec,
|
|
||||||
Texture,
|
Texture,
|
||||||
SceneTable,
|
|
||||||
LocalAabbBuffer,
|
LocalAabbBuffer,
|
||||||
CameraFrustum,
|
|
||||||
BooleanFlagBuffer,
|
BooleanFlagBuffer,
|
||||||
|
PipelineIndexStream,
|
||||||
|
PipelineActivation,
|
||||||
DrawStream,
|
DrawStream,
|
||||||
DepthStencilConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
@@ -21,7 +19,7 @@ pub enum ExecutionClass {
|
|||||||
CpuPreparation,
|
CpuPreparation,
|
||||||
Compute,
|
Compute,
|
||||||
Render,
|
Render,
|
||||||
Present,
|
Frame,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
@@ -48,8 +46,6 @@ pub enum InputRole {
|
|||||||
SampledTexture,
|
SampledTexture,
|
||||||
ColorTarget { location: u32 },
|
ColorTarget { location: u32 },
|
||||||
DepthTarget,
|
DepthTarget,
|
||||||
Present,
|
|
||||||
Configuration,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
@@ -119,48 +115,42 @@ const REQUIRED: InputCardinality = InputCardinality::RequiredOne;
|
|||||||
const OPTIONAL: InputCardinality = InputCardinality::OptionalOne;
|
const OPTIONAL: InputCardinality = InputCardinality::OptionalOne;
|
||||||
const NONE_IN: &[InputSocketContract] = &[];
|
const NONE_IN: &[InputSocketContract] = &[];
|
||||||
const NONE_OUT: &[OutputSocketContract] = &[];
|
const NONE_OUT: &[OutputSocketContract] = &[];
|
||||||
const SURFACE_OUT: &[OutputSocketContract] =
|
const TEXTURE_OUT: &[OutputSocketContract] = &[output("texture", Texture, OutputMetadata::None)];
|
||||||
&[output("surface", SurfaceTarget, OutputMetadata::None)];
|
const MESH_OUT: &[OutputSocketContract] = &[
|
||||||
const SPEC_OUT: &[OutputSocketContract] = &[output("spec", TextureSpec, OutputMetadata::None)];
|
output("mesh", MeshData, OutputMetadata::None),
|
||||||
const SCENE_OUT: &[OutputSocketContract] = &[output("scene", SceneTable, OutputMetadata::None)];
|
output("localAabbs", LocalAabbBuffer, OutputMetadata::None),
|
||||||
const AABB_OUT: &[OutputSocketContract] =
|
output(
|
||||||
&[output("localAabbs", LocalAabbBuffer, OutputMetadata::None)];
|
"isVisible",
|
||||||
const FRUSTUM_OUT: &[OutputSocketContract] =
|
|
||||||
&[output("frustum", CameraFrustum, OutputMetadata::None)];
|
|
||||||
const VISIBLE_OUT: &[OutputSocketContract] = &[output(
|
|
||||||
"flags",
|
|
||||||
BooleanFlagBuffer,
|
BooleanFlagBuffer,
|
||||||
OutputMetadata::BooleanFlag {
|
OutputMetadata::BooleanFlag {
|
||||||
flag: MeshFlag::IsVisible,
|
flag: MeshFlag::IsVisible,
|
||||||
},
|
},
|
||||||
)];
|
),
|
||||||
|
output("pipelineIndices", PipelineIndexStream, OutputMetadata::None),
|
||||||
|
];
|
||||||
const CULLED_OUT: &[OutputSocketContract] = &[output(
|
const CULLED_OUT: &[OutputSocketContract] = &[output(
|
||||||
"flags",
|
"isFrustumCulled",
|
||||||
BooleanFlagBuffer,
|
BooleanFlagBuffer,
|
||||||
OutputMetadata::BooleanFlag {
|
OutputMetadata::BooleanFlag {
|
||||||
flag: MeshFlag::IsFrustumCulled,
|
flag: MeshFlag::IsFrustumCulled,
|
||||||
},
|
},
|
||||||
)];
|
)];
|
||||||
const DRAW_OUT: &[OutputSocketContract] = &[output("draws", DrawStream, OutputMetadata::None)];
|
const DRAW_OUT: &[OutputSocketContract] = &[output("draws", DrawStream, OutputMetadata::None)];
|
||||||
const CONFIG_OUT: &[OutputSocketContract] =
|
const ACTIVATION_OUT: &[OutputSocketContract] = &[output(
|
||||||
&[output("config", DepthStencilConfig, OutputMetadata::None)];
|
"activation",
|
||||||
const FORWARD_OUT: &[OutputSocketContract] = &[
|
PipelineActivation,
|
||||||
|
OutputMetadata::None,
|
||||||
|
)];
|
||||||
|
const PIPELINE_OUT: &[OutputSocketContract] = &[
|
||||||
output("color", Texture, OutputMetadata::None),
|
output("color", Texture, OutputMetadata::None),
|
||||||
output("depth", Texture, OutputMetadata::None),
|
output("depth", Texture, OutputMetadata::None),
|
||||||
];
|
];
|
||||||
const FULLSCREEN_COPY_OUT: &[OutputSocketContract] =
|
const FULLSCREEN_COPY_OUT: &[OutputSocketContract] =
|
||||||
&[output("color", Texture, OutputMetadata::None)];
|
&[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] = &[
|
const CULL_IN: &[InputSocketContract] = &[
|
||||||
input(
|
input(
|
||||||
"scene",
|
"mesh",
|
||||||
TypeConstraint::Exact(SceneTable),
|
TypeConstraint::Exact(MeshData),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::StorageRead,
|
InputRole::StorageRead,
|
||||||
),
|
),
|
||||||
@@ -170,17 +160,11 @@ const CULL_IN: &[InputSocketContract] = &[
|
|||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::StorageRead,
|
InputRole::StorageRead,
|
||||||
),
|
),
|
||||||
input(
|
|
||||||
"frustum",
|
|
||||||
TypeConstraint::Exact(CameraFrustum),
|
|
||||||
REQUIRED,
|
|
||||||
InputRole::UniformRead,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
const QUERY_IN: &[InputSocketContract] = &[
|
const QUERY_IN: &[InputSocketContract] = &[
|
||||||
input(
|
input(
|
||||||
"scene",
|
"mesh",
|
||||||
TypeConstraint::Exact(SceneTable),
|
TypeConstraint::Exact(MeshData),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::StorageRead,
|
InputRole::StorageRead,
|
||||||
),
|
),
|
||||||
@@ -197,10 +181,16 @@ const QUERY_IN: &[InputSocketContract] = &[
|
|||||||
InputRole::StorageRead,
|
InputRole::StorageRead,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const FORWARD_IN: &[InputSocketContract] = &[
|
const REGISTRY_IN: &[InputSocketContract] = &[input(
|
||||||
|
"pipelineIndices",
|
||||||
|
TypeConstraint::Exact(PipelineIndexStream),
|
||||||
|
REQUIRED,
|
||||||
|
InputRole::SemanticRead,
|
||||||
|
)];
|
||||||
|
const PIPELINE_IN: &[InputSocketContract] = &[
|
||||||
input(
|
input(
|
||||||
"scene",
|
"mesh",
|
||||||
TypeConstraint::Exact(SceneTable),
|
TypeConstraint::Exact(MeshData),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::SemanticRead,
|
InputRole::SemanticRead,
|
||||||
),
|
),
|
||||||
@@ -210,24 +200,24 @@ const FORWARD_IN: &[InputSocketContract] = &[
|
|||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::IndirectRead,
|
InputRole::IndirectRead,
|
||||||
),
|
),
|
||||||
|
input(
|
||||||
|
"activation",
|
||||||
|
TypeConstraint::Exact(PipelineActivation),
|
||||||
|
REQUIRED,
|
||||||
|
InputRole::SemanticRead,
|
||||||
|
),
|
||||||
input(
|
input(
|
||||||
"colorTarget",
|
"colorTarget",
|
||||||
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
|
TypeConstraint::Exact(Texture),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::ColorTarget { location: 0 },
|
InputRole::ColorTarget { location: 0 },
|
||||||
),
|
),
|
||||||
input(
|
input(
|
||||||
"depthTarget",
|
"depthTarget",
|
||||||
TypeConstraint::OneOf(&[TextureSpec, Texture]),
|
TypeConstraint::Exact(Texture),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::DepthTarget,
|
InputRole::DepthTarget,
|
||||||
),
|
),
|
||||||
input(
|
|
||||||
"depthStencil",
|
|
||||||
TypeConstraint::Exact(DepthStencilConfig),
|
|
||||||
REQUIRED,
|
|
||||||
InputRole::Configuration,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
|
const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
|
||||||
input(
|
input(
|
||||||
@@ -238,7 +228,7 @@ const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
|
|||||||
),
|
),
|
||||||
input(
|
input(
|
||||||
"colorTarget",
|
"colorTarget",
|
||||||
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
|
TypeConstraint::Exact(Texture),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::ColorTarget { location: 0 },
|
InputRole::ColorTarget { location: 0 },
|
||||||
),
|
),
|
||||||
@@ -258,65 +248,33 @@ const BLOOM_COMPOSITE_IN: &[InputSocketContract] = &[
|
|||||||
),
|
),
|
||||||
input(
|
input(
|
||||||
"colorTarget",
|
"colorTarget",
|
||||||
TypeConstraint::OneOf(&[SurfaceTarget, TextureSpec, Texture]),
|
TypeConstraint::Exact(Texture),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::ColorTarget { location: 0 },
|
InputRole::ColorTarget { location: 0 },
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const PRESENT_IN: &[InputSocketContract] = &[input(
|
const FRAME_OUT_IN: &[InputSocketContract] = &[input(
|
||||||
"surface",
|
"color",
|
||||||
TypeConstraint::Exact(Texture),
|
TypeConstraint::Exact(Texture),
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
InputRole::Present,
|
InputRole::SampledTexture,
|
||||||
)];
|
)];
|
||||||
|
|
||||||
pub static CONTRACTS: &[Contract] = &[
|
pub static CONTRACTS: &[Contract] = &[
|
||||||
Contract {
|
Contract {
|
||||||
key: "surface_target",
|
key: "mesh",
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: ExecutionClass::Source,
|
execution: ExecutionClass::Source,
|
||||||
inputs: NONE_IN,
|
inputs: NONE_IN,
|
||||||
outputs: SURFACE_OUT,
|
outputs: MESH_OUT,
|
||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
key: "texture_spec",
|
key: "texture",
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: ExecutionClass::Source,
|
execution: ExecutionClass::Source,
|
||||||
inputs: NONE_IN,
|
inputs: NONE_IN,
|
||||||
outputs: SPEC_OUT,
|
outputs: TEXTURE_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,
|
|
||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
@@ -336,19 +294,19 @@ pub static CONTRACTS: &[Contract] = &[
|
|||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
key: "depth_stencil_config",
|
key: "pipeline_registry",
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: ExecutionClass::Source,
|
execution: ExecutionClass::CpuPreparation,
|
||||||
inputs: NONE_IN,
|
inputs: REGISTRY_IN,
|
||||||
outputs: CONFIG_OUT,
|
outputs: ACTIVATION_OUT,
|
||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
key: "legacy_forward",
|
key: "pipeline",
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: ExecutionClass::Render,
|
execution: ExecutionClass::Render,
|
||||||
inputs: FORWARD_IN,
|
inputs: PIPELINE_IN,
|
||||||
outputs: FORWARD_OUT,
|
outputs: PIPELINE_OUT,
|
||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
@@ -400,10 +358,10 @@ pub static CONTRACTS: &[Contract] = &[
|
|||||||
inherently_observable: false,
|
inherently_observable: false,
|
||||||
},
|
},
|
||||||
Contract {
|
Contract {
|
||||||
key: "present",
|
key: "frame_out",
|
||||||
version: 1,
|
version: 1,
|
||||||
execution: ExecutionClass::Present,
|
execution: ExecutionClass::Frame,
|
||||||
inputs: PRESENT_IN,
|
inputs: FRAME_OUT_IN,
|
||||||
outputs: NONE_OUT,
|
outputs: NONE_OUT,
|
||||||
inherently_observable: true,
|
inherently_observable: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,4 +49,4 @@ impl GraphError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
pub(crate) mod tests;
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ pub struct CompiledResource {
|
|||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum ResourcePlan {
|
pub enum ResourcePlan {
|
||||||
SurfaceTarget {
|
TextureSource {
|
||||||
family: u32,
|
|
||||||
},
|
|
||||||
TextureSpec {
|
|
||||||
family: u32,
|
family: u32,
|
||||||
residency: TextureResidency,
|
residency: TextureResidency,
|
||||||
descriptor: NormalizedTextureDescriptor,
|
descriptor: NormalizedTextureDescriptor,
|
||||||
@@ -49,20 +46,22 @@ pub enum ResourcePlan {
|
|||||||
stored: bool,
|
stored: bool,
|
||||||
allocation: Option<AllocationRef>,
|
allocation: Option<AllocationRef>,
|
||||||
},
|
},
|
||||||
SceneTable,
|
MeshData,
|
||||||
LocalAabbBuffer {
|
LocalAabbBuffer {
|
||||||
scene: u32,
|
mesh: u32,
|
||||||
},
|
},
|
||||||
CameraFrustum,
|
|
||||||
BooleanFlagBuffer {
|
BooleanFlagBuffer {
|
||||||
scene: u32,
|
mesh: u32,
|
||||||
flag: MeshFlag,
|
flag: MeshFlag,
|
||||||
},
|
},
|
||||||
DrawStream {
|
PipelineIndexStream {
|
||||||
scene: u32,
|
mesh: u32,
|
||||||
},
|
},
|
||||||
DepthStencilConfig {
|
PipelineActivation {
|
||||||
config: NormalizedDepthStencil,
|
pipeline_indices: u32,
|
||||||
|
},
|
||||||
|
DrawStream {
|
||||||
|
mesh: u32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +103,12 @@ pub enum ExecutionKind {
|
|||||||
color_attachments: Vec<ColorAttachmentPlan>,
|
color_attachments: Vec<ColorAttachmentPlan>,
|
||||||
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
||||||
},
|
},
|
||||||
Present {
|
FrameOut {
|
||||||
surface: u32,
|
color: u32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ComputeWork {
|
pub enum ComputeWork {
|
||||||
FrustumCull,
|
FrustumCull,
|
||||||
@@ -184,29 +183,29 @@ pub enum AccessMode {
|
|||||||
store: StoreOp,
|
store: StoreOp,
|
||||||
full_overwrite: bool,
|
full_overwrite: bool,
|
||||||
},
|
},
|
||||||
Present,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum NormalizedParameters {
|
pub enum NormalizedParameters {
|
||||||
SurfaceTarget,
|
Texture {
|
||||||
TextureSpec {
|
|
||||||
residency: TextureResidency,
|
residency: TextureResidency,
|
||||||
texture: NormalizedTextureDescriptor,
|
descriptor: NormalizedTextureDescriptor,
|
||||||
|
},
|
||||||
|
Mesh,
|
||||||
|
FrustumCull {
|
||||||
|
camera: ActiveCamera,
|
||||||
},
|
},
|
||||||
SceneTable,
|
|
||||||
LocalAabbBuffer,
|
|
||||||
CameraFrustum,
|
|
||||||
VisibilityFlags,
|
|
||||||
FrustumCull,
|
|
||||||
MeshQuery {
|
MeshQuery {
|
||||||
filters: [NormalizedMeshFilter; 2],
|
visible_predicate: RuntimePredicate,
|
||||||
|
frustum_culled_predicate: RuntimePredicate,
|
||||||
},
|
},
|
||||||
DepthStencilConfig {
|
PipelineRegistry,
|
||||||
config: NormalizedDepthStencil,
|
Pipeline {
|
||||||
},
|
pipeline: String,
|
||||||
LegacyForward {
|
depth_compare: CompareFunction,
|
||||||
|
depth_write_enabled: bool,
|
||||||
|
clear_depth: f32,
|
||||||
clear_color: [f64; 4],
|
clear_color: [f64; 4],
|
||||||
},
|
},
|
||||||
FullscreenCopy,
|
FullscreenCopy,
|
||||||
@@ -227,22 +226,13 @@ pub enum NormalizedParameters {
|
|||||||
LuminanceEdge {
|
LuminanceEdge {
|
||||||
strength: f32,
|
strength: f32,
|
||||||
},
|
},
|
||||||
Present,
|
FrameOut,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct NormalizedMeshFilter {
|
pub enum ActiveCamera {
|
||||||
pub flag: MeshFlag,
|
Active,
|
||||||
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, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
|
||||||
@@ -288,9 +278,6 @@ pub struct TextureFamilyKey {
|
|||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum TextureFamilySource {
|
pub enum TextureFamilySource {
|
||||||
ImportedSurface {
|
|
||||||
resource: u32,
|
|
||||||
},
|
|
||||||
AuthoredTexture {
|
AuthoredTexture {
|
||||||
resource: u32,
|
resource: u32,
|
||||||
residency: TextureResidency,
|
residency: TextureResidency,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -114,11 +114,13 @@ pub enum TriStatePredicate {
|
|||||||
RequiredFalse,
|
RequiredFalse,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct MeshFilter {
|
pub enum RuntimePredicate {
|
||||||
pub flag: MeshFlag,
|
Any,
|
||||||
pub predicate: TriStatePredicate,
|
RequiredTrue,
|
||||||
|
RequiredFalse,
|
||||||
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
|||||||
+1963
-462
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; }
|
if (i >= params.count) { return; }
|
||||||
let draw_meta = metadata[i];
|
let draw_meta = metadata[i];
|
||||||
var selected = true;
|
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);
|
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);
|
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);
|
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};
|
||||||
|
|||||||
+39
-39
@@ -56,25 +56,8 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
materials: &MaterialResources,
|
materials: &MaterialResources,
|
||||||
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||||
) -> Result<(), &'static str> {
|
) -> Result<(), &'static str> {
|
||||||
use crate::render_graph::{
|
use crate::render_graph::{ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
|
||||||
ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, ResourcePlan, StoreOp,
|
|
||||||
};
|
|
||||||
let view = |resource: u32| -> Result<&wgpu::TextureView, &'static str> {
|
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
|
let a = active
|
||||||
.runtime
|
.runtime
|
||||||
.allocations
|
.allocations
|
||||||
@@ -93,15 +76,16 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
for (execution_index, prepared) in active.executions.iter().enumerate() {
|
for (execution_index, prepared) in active.executions.iter().enumerate() {
|
||||||
let profile_id = &active.graph.executions[execution_index].id;
|
let profile_id = &active.graph.executions[execution_index].id;
|
||||||
match prepared {
|
match prepared {
|
||||||
|
PreparedExecution::PipelineRegistry => {}
|
||||||
PreparedExecution::FrustumCull => {
|
PreparedExecution::FrustumCull => {
|
||||||
gpu.encode_frustum_cull(encoder, profile.as_deref_mut(), profile_id);
|
gpu.encode_frustum_cull(encoder, profile.as_deref_mut(), profile_id);
|
||||||
}
|
}
|
||||||
PreparedExecution::MeshQuery => {
|
PreparedExecution::MeshQuery => {
|
||||||
gpu.encode_mesh_query(encoder, profile.as_deref_mut(), profile_id);
|
gpu.encode_mesh_query(encoder, profile.as_deref_mut(), profile_id);
|
||||||
}
|
}
|
||||||
PreparedExecution::Present => {}
|
|
||||||
PreparedExecution::Fullscreen {
|
PreparedExecution::Fullscreen {
|
||||||
execution,
|
execution,
|
||||||
|
frame_out,
|
||||||
bind_group,
|
bind_group,
|
||||||
pipeline,
|
pipeline,
|
||||||
..
|
..
|
||||||
@@ -111,6 +95,18 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
.executions
|
.executions
|
||||||
.get(*execution)
|
.get(*execution)
|
||||||
.ok_or(" execution out of bounds")?;
|
.ok_or(" execution out of bounds")?;
|
||||||
|
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 {
|
let ExecutionKind::Render {
|
||||||
color_attachments, ..
|
color_attachments, ..
|
||||||
} = &execution.kind
|
} = &execution.kind
|
||||||
@@ -120,13 +116,9 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
let color = color_attachments
|
let color = color_attachments
|
||||||
.first()
|
.first()
|
||||||
.ok_or("fullscreen target missing")?;
|
.ok_or("fullscreen target missing")?;
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
(
|
||||||
label: Some(&execution.id),
|
view(color.resource)?,
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
wgpu::Operations {
|
||||||
view: view(color.resource)?,
|
|
||||||
depth_slice: None,
|
|
||||||
resolve_target: None,
|
|
||||||
ops: wgpu::Operations {
|
|
||||||
load: match color.load {
|
load: match color.load {
|
||||||
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
|
NormalizedColorLoad::Load => wgpu::LoadOp::Load,
|
||||||
NormalizedColorLoad::Clear { value } => {
|
NormalizedColorLoad::Clear { value } => {
|
||||||
@@ -144,6 +136,15 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
wgpu::StoreOp::Discard
|
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,
|
depth_stencil_attachment: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
@@ -155,9 +156,10 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
pass.set_bind_group(0, bind_group, &[]);
|
pass.set_bind_group(0, bind_group, &[]);
|
||||||
pass.draw(0..3, 0..1);
|
pass.draw(0..3, 0..1);
|
||||||
}
|
}
|
||||||
PreparedExecution::LegacyForward {
|
PreparedExecution::Pipeline {
|
||||||
execution,
|
execution,
|
||||||
variants,
|
base,
|
||||||
|
variant,
|
||||||
} => {
|
} => {
|
||||||
let execution = active
|
let execution = active
|
||||||
.graph
|
.graph
|
||||||
@@ -169,10 +171,10 @@ pub(crate) fn encode_compiled<T: Scene>(
|
|||||||
depth_stencil,
|
depth_stencil,
|
||||||
} = &execution.kind
|
} = &execution.kind
|
||||||
else {
|
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 color = color_attachments.first().ok_or("pipeline color missing")?;
|
||||||
let depth = depth_stencil.as_ref().ok_or("legacy depth missing")?;
|
let depth = depth_stencil.as_ref().ok_or("pipeline depth missing")?;
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some(&execution.id),
|
label: Some(&execution.id),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
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_vertex_buffer(4, t.slice(..));
|
||||||
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
|
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
for draw in &gpu.draws {
|
for draw in &gpu.draws {
|
||||||
|
if draw.pipeline != *base {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let slot = draw.instances.start as u64;
|
let slot = draw.instances.start as u64;
|
||||||
let start = slot
|
let start = slot
|
||||||
* std::mem::size_of::<crate::renderer::gpu_scene::GpuInstance>() as u64;
|
* std::mem::size_of::<crate::renderer::gpu_scene::GpuInstance>() as u64;
|
||||||
pass.set_vertex_buffer(3, inst.slice(start..start + 112));
|
pass.set_vertex_buffer(3, inst.slice(start..start + 112));
|
||||||
let key = variants
|
pass.set_pipeline(variant);
|
||||||
.iter()
|
|
||||||
.find(|(base, _)| *base == draw.pipeline)
|
|
||||||
.map(|x| &x.1)
|
|
||||||
.ok_or("pipeline variant missing")?;
|
|
||||||
pass.set_pipeline(key);
|
|
||||||
if pipelines.requires_material(draw.pipeline) {
|
if pipelines.requires_material(draw.pipeline) {
|
||||||
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
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>,
|
profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||||
) {
|
) {
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("Render pass"),
|
label: Some("Immediate pipeline pass"),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
view: color,
|
view: color,
|
||||||
@@ -298,7 +298,7 @@ pub(crate) fn encode_immediate<T: Scene>(
|
|||||||
stencil_ops: None,
|
stencil_ops: None,
|
||||||
}),
|
}),
|
||||||
occlusion_query_set: 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);
|
encode_scene(&mut pass, scene, gpu, pipelines, materials);
|
||||||
}
|
}
|
||||||
@@ -85,8 +85,8 @@ impl GpuScenePlan {
|
|||||||
self::GpuScenePlan::build_with_query(
|
self::GpuScenePlan::build_with_query(
|
||||||
data,
|
data,
|
||||||
crate::render_graph::MeshQueryRuntimeKey {
|
crate::render_graph::MeshQueryRuntimeKey {
|
||||||
visible: crate::render_graph::TriStatePredicate::RequiredTrue,
|
visible: crate::render_graph::RuntimePredicate::RequiredTrue,
|
||||||
frustum_culled: crate::render_graph::TriStatePredicate::Any,
|
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,6 @@ pub struct BufferSlot {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GpuSceneCache {
|
pub struct GpuSceneCache {
|
||||||
revision: Option<u64>,
|
revision: Option<u64>,
|
||||||
query: Option<crate::render_graph::MeshQueryRuntimeKey>,
|
|
||||||
pub positions: BufferSlot,
|
pub positions: BufferSlot,
|
||||||
pub normals: BufferSlot,
|
pub normals: BufferSlot,
|
||||||
pub uvs: BufferSlot,
|
pub uvs: BufferSlot,
|
||||||
@@ -310,11 +309,12 @@ struct CullingParams {
|
|||||||
_pad: u32,
|
_pad: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn predicate_code(value: crate::render_graph::TriStatePredicate) -> u32 {
|
fn predicate_code(value: crate::render_graph::RuntimePredicate) -> u32 {
|
||||||
match value {
|
match value {
|
||||||
crate::render_graph::TriStatePredicate::Any => 0,
|
crate::render_graph::RuntimePredicate::Any => 0,
|
||||||
crate::render_graph::TriStatePredicate::RequiredTrue => 1,
|
crate::render_graph::RuntimePredicate::RequiredTrue => 1,
|
||||||
crate::render_graph::TriStatePredicate::RequiredFalse => 2,
|
crate::render_graph::RuntimePredicate::RequiredFalse => 2,
|
||||||
|
crate::render_graph::RuntimePredicate::Never => 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +330,8 @@ impl GpuSceneCache {
|
|||||||
queue,
|
queue,
|
||||||
data,
|
data,
|
||||||
crate::render_graph::MeshQueryRuntimeKey {
|
crate::render_graph::MeshQueryRuntimeKey {
|
||||||
visible: crate::render_graph::TriStatePredicate::RequiredTrue,
|
visible: crate::render_graph::RuntimePredicate::RequiredTrue,
|
||||||
frustum_culled: crate::render_graph::TriStatePredicate::Any,
|
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -343,14 +343,13 @@ impl GpuSceneCache {
|
|||||||
data: &SceneFramePlan,
|
data: &SceneFramePlan,
|
||||||
query: crate::render_graph::MeshQueryRuntimeKey,
|
query: crate::render_graph::MeshQueryRuntimeKey,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if self.revision == Some(data.revision) && self.query == Some(query) {
|
if self.revision == Some(data.revision) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let plan = GpuScenePlan::build_with_query(data, query).map_err(str::to_owned)?;
|
let plan = GpuScenePlan::build_with_query(data, query).map_err(str::to_owned)?;
|
||||||
if plan.draws.is_empty() {
|
if plan.draws.is_empty() {
|
||||||
self.draws.clear();
|
self.draws.clear();
|
||||||
self.revision = Some(data.revision);
|
self.revision = Some(data.revision);
|
||||||
self.query = Some(query);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let maximum = device.limits().max_buffer_size;
|
let maximum = device.limits().max_buffer_size;
|
||||||
@@ -485,7 +484,6 @@ impl GpuSceneCache {
|
|||||||
self.draws = plan.draws;
|
self.draws = plan.draws;
|
||||||
self.rebuild_compute(device)?;
|
self.rebuild_compute(device)?;
|
||||||
self.revision = Some(data.revision);
|
self.revision = Some(data.revision);
|
||||||
self.query = Some(query);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,11 +640,28 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn mesh_query_source_guards_optional_flag_buffer_reads() {
|
fn mesh_query_source_guards_optional_flag_buffer_reads() {
|
||||||
let source = include_str!("culling.wgsl");
|
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 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();
|
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 {
|
for binding in 0..=6 {
|
||||||
assert!(source.contains(&format!("@binding({binding})")));
|
assert!(source.contains(&format!("@binding({binding})")));
|
||||||
}
|
}
|
||||||
|
|||||||
+134
-82
@@ -35,17 +35,19 @@ struct GpuTextureSlot {
|
|||||||
enum PreparedExecution {
|
enum PreparedExecution {
|
||||||
FrustumCull,
|
FrustumCull,
|
||||||
MeshQuery,
|
MeshQuery,
|
||||||
LegacyForward {
|
PipelineRegistry,
|
||||||
|
Pipeline {
|
||||||
execution: usize,
|
execution: usize,
|
||||||
variants: Vec<(crate::render_data::PipelineKey, wgpu::RenderPipeline)>,
|
base: crate::render_data::PipelineKey,
|
||||||
|
variant: wgpu::RenderPipeline,
|
||||||
},
|
},
|
||||||
Fullscreen {
|
Fullscreen {
|
||||||
execution: usize,
|
execution: usize,
|
||||||
|
frame_out: bool,
|
||||||
bind_group: wgpu::BindGroup,
|
bind_group: wgpu::BindGroup,
|
||||||
pipeline: wgpu::RenderPipeline,
|
pipeline: wgpu::RenderPipeline,
|
||||||
_uniform: wgpu::Buffer,
|
_uniform: wgpu::Buffer,
|
||||||
},
|
},
|
||||||
Present,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActiveCompiledGraph {
|
struct ActiveCompiledGraph {
|
||||||
@@ -81,7 +83,10 @@ fn resolve_culling_frustum(
|
|||||||
query: crate::render_graph::MeshQueryRuntimeKey,
|
query: crate::render_graph::MeshQueryRuntimeKey,
|
||||||
read: impl FnOnce() -> Option<Result<[[f32; 4]; 6], crate::camera::FrustumError>>,
|
read: impl FnOnce() -> Option<Result<[[f32; 4]; 6], crate::camera::FrustumError>>,
|
||||||
) -> Result<Option<[[f32; 4]; 6]>, crate::render_graph::GraphError> {
|
) -> 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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
match read() {
|
match read() {
|
||||||
@@ -177,18 +182,25 @@ fn resolve_switch_request(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod switch_request_tests {
|
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::*;
|
use super::*;
|
||||||
|
|
||||||
fn query(visible: crate::render_graph::TriStatePredicate) -> UploadGraph {
|
fn query(visible: crate::render_graph::RuntimePredicate) -> UploadGraph {
|
||||||
UploadGraph::Compiled(crate::render_graph::MeshQueryRuntimeKey {
|
UploadGraph::Compiled(crate::render_graph::MeshQueryRuntimeKey {
|
||||||
visible,
|
visible,
|
||||||
frustum_culled: crate::render_graph::TriStatePredicate::Any,
|
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn upload_selection_follows_the_graph_rendered_for_the_commit_frame() {
|
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 =
|
let selected =
|
||||||
|pending, active| upload_query_for_render(pending, active).map(|query| query.visible);
|
|pending, active| upload_query_for_render(pending, active).map(|query| query.visible);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -214,7 +226,7 @@ mod switch_request_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn frustum_preflight_skips_any_and_distinguishes_missing_from_invalid() {
|
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 {
|
let query = |frustum_culled| crate::render_graph::MeshQueryRuntimeKey {
|
||||||
visible: RequiredTrue,
|
visible: RequiredTrue,
|
||||||
frustum_culled,
|
frustum_culled,
|
||||||
@@ -245,8 +257,10 @@ mod switch_request_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolves_at_command_boundary_before_gpu_work() {
|
fn resolves_at_command_boundary_before_gpu_work() {
|
||||||
let mut registry = crate::render_graph::Registry::default();
|
let mut registry = crate::render_graph::Registry::default();
|
||||||
let bytes = br#"{"schemaVersion":2,"graphId":"switch","revision":1,"nodes":[]}"#;
|
let mut graph = crate::render_graph::tests::full_cull_graph();
|
||||||
let (id, _) = registry.compile(bytes).unwrap();
|
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 active = "existing_graph";
|
||||||
let pending: Option<&str> = None;
|
let pending: Option<&str> = None;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -279,9 +293,7 @@ mod switch_request_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resize_restart_snapshot_remains_bound_to_its_immutable_registry_revision() {
|
fn resize_restart_snapshot_remains_bound_to_its_immutable_registry_revision() {
|
||||||
let mut registry = crate::render_graph::Registry::default();
|
let mut registry = crate::render_graph::Registry::default();
|
||||||
let (id, _) = registry
|
let (id, _) = registry.compile(&valid_compile_graph("resize", 1)).unwrap();
|
||||||
.compile(br#"{"schemaVersion":2,"graphId":"resize","revision":1,"nodes":[]}"#)
|
|
||||||
.unwrap();
|
|
||||||
let revision_one = registry.get(id).unwrap().clone();
|
let revision_one = registry.get(id).unwrap().clone();
|
||||||
let in_flight = InFlightPreparation {
|
let in_flight = InFlightPreparation {
|
||||||
token: 1,
|
token: 1,
|
||||||
@@ -289,9 +301,7 @@ mod switch_request_tests {
|
|||||||
purpose: PreparationPurpose::Resize,
|
purpose: PreparationPurpose::Resize,
|
||||||
graph: revision_one,
|
graph: revision_one,
|
||||||
};
|
};
|
||||||
let (revision_two_id, _) = registry
|
let (revision_two_id, _) = registry.compile(&valid_compile_graph("resize", 2)).unwrap();
|
||||||
.compile(br#"{"schemaVersion":2,"graphId":"resize","revision":2,"nodes":[]}"#)
|
|
||||||
.unwrap();
|
|
||||||
let original = registry.get(id).unwrap();
|
let original = registry.get(id).unwrap();
|
||||||
let revision_two = registry.get(revision_two_id).unwrap();
|
let revision_two = registry.get(revision_two_id).unwrap();
|
||||||
assert_eq!(in_flight.graph.revision, 1);
|
assert_eq!(in_flight.graph.revision, 1);
|
||||||
@@ -728,6 +738,26 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
) -> Result<ActiveCompiledGraph, crate::render_graph::GraphError> {
|
) -> Result<ActiveCompiledGraph, crate::render_graph::GraphError> {
|
||||||
use crate::render_graph::*;
|
use crate::render_graph::*;
|
||||||
let fail = |message| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", message);
|
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());
|
let mut textures = Vec::with_capacity(runtime.allocations.classes.len());
|
||||||
for class in &runtime.allocations.classes {
|
for class in &runtime.allocations.classes {
|
||||||
let mut gpu_class = Vec::with_capacity(class.slots.len());
|
let mut gpu_class = Vec::with_capacity(class.slots.len());
|
||||||
@@ -845,36 +875,52 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
match execution.executor.key.as_str() {
|
match execution.executor.key.as_str() {
|
||||||
"frustum_cull" => executions.push(PreparedExecution::FrustumCull),
|
"frustum_cull" => executions.push(PreparedExecution::FrustumCull),
|
||||||
"mesh_query" => executions.push(PreparedExecution::MeshQuery),
|
"mesh_query" => executions.push(PreparedExecution::MeshQuery),
|
||||||
"present" => executions.push(PreparedExecution::Present),
|
"frame_out" | "fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
|
||||||
"fullscreen_copy" | "tone_map" | "bloom_extract" | "bloom_blur"
|
|
||||||
| "bloom_composite" | "luminance_edge" => {
|
| "bloom_composite" | "luminance_edge" => {
|
||||||
let sampled: Vec<_> = execution
|
let frame_out = execution.executor.key == "frame_out";
|
||||||
.accesses
|
let (source, second) = if frame_out {
|
||||||
.iter()
|
let ExecutionKind::FrameOut { color } = execution.kind else {
|
||||||
.filter(|a| matches!(a.mode, AccessMode::SampledTexture))
|
return Err(fail("frame_out kind mismatch"));
|
||||||
.map(|a| a.resource)
|
};
|
||||||
.collect();
|
(color, color)
|
||||||
let source = *sampled
|
} else {
|
||||||
.first()
|
match execution.inputs.as_slice() {
|
||||||
.ok_or_else(|| fail("fullscreen source missing"))?;
|
[source, _color_target] => (source.resource, source.resource),
|
||||||
let second = *sampled.get(1).unwrap_or(&source);
|
[source, bloom, _color_target]
|
||||||
let values: [f32; 8] = match execution.parameters {
|
if execution.executor.key == "bloom_composite" =>
|
||||||
NormalizedParameters::ToneMap { exposure } => {
|
{
|
||||||
[exposure, 0., 0., 0., 0., 0., 0., 0.]
|
(source.resource, bloom.resource)
|
||||||
}
|
}
|
||||||
NormalizedParameters::BloomExtract { threshold, knee } => {
|
_ => return Err(fail("fullscreen inputs mismatch")),
|
||||||
[threshold, knee, 0., 0., 0., 0., 0., 0.]
|
|
||||||
}
|
}
|
||||||
NormalizedParameters::BloomBlur { direction, radius } => {
|
};
|
||||||
[direction[0], direction[1], radius, 0., 0., 0., 0., 0.]
|
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.]
|
||||||
}
|
}
|
||||||
NormalizedParameters::BloomComposite { intensity } => {
|
(
|
||||||
[intensity, 0., 0., 0., 0., 0., 0., 0.]
|
"bloom_extract",
|
||||||
}
|
NormalizedParameters::BloomExtract { threshold, knee },
|
||||||
NormalizedParameters::LuminanceEdge { strength } => {
|
) => [*threshold, *knee, 0., 0., 0., 0., 0., 0.],
|
||||||
[strength, 0., 0., 0., 0., 0., 0., 0.]
|
(
|
||||||
}
|
"bloom_blur",
|
||||||
_ => [0.; 8],
|
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;
|
use wgpu::util::DeviceExt;
|
||||||
let uniform =
|
let uniform =
|
||||||
@@ -885,6 +931,9 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
contents: bytemuck::cast_slice(&values),
|
contents: bytemuck::cast_slice(&values),
|
||||||
usage: wgpu::BufferUsages::UNIFORM,
|
usage: wgpu::BufferUsages::UNIFORM,
|
||||||
});
|
});
|
||||||
|
let target_format = if frame_out {
|
||||||
|
runtime.surface.format
|
||||||
|
} else {
|
||||||
let ExecutionKind::Render {
|
let ExecutionKind::Render {
|
||||||
color_attachments, ..
|
color_attachments, ..
|
||||||
} = &execution.kind
|
} = &execution.kind
|
||||||
@@ -895,7 +944,22 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| fail("fullscreen target missing"))?
|
.ok_or_else(|| fail("fullscreen target missing"))?
|
||||||
.resource;
|
.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 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 entry = match execution.executor.key.as_str() {
|
let entry = match execution.executor.key.as_str() {
|
||||||
"fullscreen_copy" => "fs_copy",
|
"fullscreen_copy" => "fs_copy",
|
||||||
"tone_map" => "fs_tone_map",
|
"tone_map" => "fs_tone_map",
|
||||||
@@ -903,7 +967,8 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
"bloom_blur" => "fs_bloom_blur",
|
"bloom_blur" => "fs_bloom_blur",
|
||||||
"bloom_composite" => "fs_bloom_composite",
|
"bloom_composite" => "fs_bloom_composite",
|
||||||
"luminance_edge" => "fs_luminance_edge",
|
"luminance_edge" => "fs_luminance_edge",
|
||||||
_ => unreachable!(),
|
"frame_out" => "fs_copy",
|
||||||
|
_ => return Err(fail("fullscreen executor mismatch")),
|
||||||
};
|
};
|
||||||
let pipeline = self.context.device.create_render_pipeline(
|
let pipeline = self.context.device.create_render_pipeline(
|
||||||
&wgpu::RenderPipelineDescriptor {
|
&wgpu::RenderPipelineDescriptor {
|
||||||
@@ -963,36 +1028,25 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
});
|
});
|
||||||
executions.push(PreparedExecution::Fullscreen {
|
executions.push(PreparedExecution::Fullscreen {
|
||||||
execution: index,
|
execution: index,
|
||||||
|
frame_out,
|
||||||
bind_group,
|
bind_group,
|
||||||
pipeline,
|
pipeline,
|
||||||
_uniform: uniform,
|
_uniform: uniform,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"legacy_forward" => {
|
"pipeline_registry" => executions.push(PreparedExecution::PipelineRegistry),
|
||||||
|
"pipeline" => {
|
||||||
let ExecutionKind::Render {
|
let ExecutionKind::Render {
|
||||||
color_attachments,
|
color_attachments,
|
||||||
depth_stencil,
|
depth_stencil,
|
||||||
} = &execution.kind
|
} = &execution.kind
|
||||||
else {
|
else {
|
||||||
return Err(fail("legacy forward is not render"));
|
return Err(fail("pipeline is not render"));
|
||||||
};
|
};
|
||||||
let color = color_attachments
|
let color = color_attachments
|
||||||
.first()
|
.first()
|
||||||
.ok_or_else(|| fail("legacy color missing"))?;
|
.ok_or_else(|| fail("pipeline color missing"))?;
|
||||||
let color_is_surface = graph
|
let color_format = {
|
||||||
.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 {
|
|
||||||
let a = runtime
|
let a = runtime
|
||||||
.allocations
|
.allocations
|
||||||
.resource_allocations
|
.resource_allocations
|
||||||
@@ -1027,19 +1081,21 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
.ok_or_else(|| fail("depth allocation invalid"))
|
.ok_or_else(|| fail("depth allocation invalid"))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let config = execution
|
let NormalizedParameters::Pipeline {
|
||||||
.inputs
|
pipeline: _,
|
||||||
.iter()
|
depth_compare,
|
||||||
.filter_map(|i| graph.resources.get(i.resource as usize))
|
depth_write_enabled,
|
||||||
.find_map(|r| {
|
..
|
||||||
if let ResourcePlan::DepthStencilConfig { config } = r.plan {
|
} = &execution.parameters
|
||||||
Some(config)
|
else {
|
||||||
} else {
|
return Err(fail("pipeline parameters mismatch"));
|
||||||
None
|
};
|
||||||
}
|
let base = resolved_pipelines
|
||||||
})
|
.get(index)
|
||||||
.ok_or_else(|| fail("depth config missing"))?;
|
.copied()
|
||||||
let compare = match config.depth_compare {
|
.flatten()
|
||||||
|
.ok_or_else(|| fail("resolved pipeline missing"))?;
|
||||||
|
let compare = match depth_compare {
|
||||||
CompareFunction::Never => wgpu::CompareFunction::Never,
|
CompareFunction::Never => wgpu::CompareFunction::Never,
|
||||||
CompareFunction::Less => wgpu::CompareFunction::Less,
|
CompareFunction::Less => wgpu::CompareFunction::Less,
|
||||||
CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual,
|
CompareFunction::LessEqual => wgpu::CompareFunction::LessEqual,
|
||||||
@@ -1049,9 +1105,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual,
|
CompareFunction::NotEqual => wgpu::CompareFunction::NotEqual,
|
||||||
CompareFunction::Always => wgpu::CompareFunction::Always,
|
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
|
let variant = self
|
||||||
.resources
|
.resources
|
||||||
.create_target_variant(
|
.create_target_variant(
|
||||||
@@ -1060,14 +1113,13 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
color_format,
|
color_format,
|
||||||
depth_format,
|
depth_format,
|
||||||
compare,
|
compare,
|
||||||
config.depth_write_enabled,
|
*depth_write_enabled,
|
||||||
)
|
)
|
||||||
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
.map_err(|e| GraphError::new("GRAPH_RUNTIME_PLAN_INVALID", e))?;
|
||||||
variants.push((base, variant));
|
executions.push(PreparedExecution::Pipeline {
|
||||||
}
|
|
||||||
executions.push(PreparedExecution::LegacyForward {
|
|
||||||
execution: index,
|
execution: index,
|
||||||
variants,
|
base,
|
||||||
|
variant,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => return Err(fail("unsupported prepared execution")),
|
_ => return Err(fail("unsupported prepared execution")),
|
||||||
|
|||||||
@@ -372,36 +372,6 @@ impl PipelineLibrary {
|
|||||||
&& self.specs[key.get() as usize].layout == self.material_layout
|
&& 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(
|
pub fn create_target_variant(
|
||||||
&self,
|
&self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
|
|||||||
+266
-40
@@ -56,14 +56,20 @@ const sourceMaps = new WeakMap();
|
|||||||
export const getSourceMap = (ir) => sourceMaps.get(ir);
|
export const getSourceMap = (ir) => sourceMaps.get(ir);
|
||||||
export const mapAuthoringDiagnostic = (ir, diagnostic) => {
|
export const mapAuthoringDiagnostic = (ir, diagnostic) => {
|
||||||
const details = diagnostic?.details;
|
const details = diagnostic?.details;
|
||||||
const path = [details?.path, diagnostic?.path, details?.field, diagnostic?.field]
|
const path = [
|
||||||
.find((value) => typeof value === "string");
|
details?.path,
|
||||||
|
diagnostic?.path,
|
||||||
|
details?.field,
|
||||||
|
diagnostic?.field,
|
||||||
|
].find((value) => typeof value === "string");
|
||||||
const map = getSourceMap(ir);
|
const map = getSourceMap(ir);
|
||||||
let match;
|
let match;
|
||||||
if (path && map)
|
if (path && map)
|
||||||
for (const key of Object.keys(map))
|
for (const key of Object.keys(map))
|
||||||
if (
|
if (
|
||||||
(path === key || path.startsWith(`${key}.`) || path.startsWith(`${key}[`)) &&
|
(path === key ||
|
||||||
|
path.startsWith(`${key}.`) ||
|
||||||
|
path.startsWith(`${key}[`)) &&
|
||||||
(!match || key.length > match.length)
|
(!match || key.length > match.length)
|
||||||
)
|
)
|
||||||
match = key;
|
match = key;
|
||||||
@@ -80,33 +86,49 @@ export const mapAuthoringDiagnostic = (ir, diagnostic) => {
|
|||||||
const mapValuePaths = (paths, path, source, value) => {
|
const mapValuePaths = (paths, path, source, value) => {
|
||||||
paths[path] = source;
|
paths[path] = source;
|
||||||
if (Array.isArray(value))
|
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))
|
else if (object(value))
|
||||||
for (const key of Object.keys(value))
|
for (const key of Object.keys(value))
|
||||||
mapValuePaths(paths, `${path}.${key}`, source, value[key]);
|
mapValuePaths(paths, `${path}.${key}`, source, value[key]);
|
||||||
};
|
};
|
||||||
|
|
||||||
function parameterValue(raw, schema, nodeId, key) {
|
function parameterValue(raw, schema, nodeId, key) {
|
||||||
const expected = schema.type === "json" ? "json" : schema.type;
|
if (!exactKeys(raw, ["kind", "value"]) || raw.kind !== 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))
|
|
||||||
)
|
|
||||||
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
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));
|
return canonical(structuredClone(raw.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adaptFxNodeSnapshot(raw, revision = 1) {
|
export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||||
try {
|
try {
|
||||||
const rootKeys = ["graphId", "catalogVersion", "nodes", "links", "metadata", "version"];
|
const rootKeys = [
|
||||||
|
"graphId",
|
||||||
|
"catalogVersion",
|
||||||
|
"nodes",
|
||||||
|
"links",
|
||||||
|
"metadata",
|
||||||
|
"version",
|
||||||
|
];
|
||||||
if (
|
if (
|
||||||
!exactKeys(raw, rootKeys) ||
|
!exactKeys(raw, rootKeys) ||
|
||||||
!Array.isArray(raw.nodes) ||
|
!Array.isArray(raw.nodes) ||
|
||||||
@@ -117,9 +139,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
fail("AUTHORING_SHAPE");
|
fail("AUTHORING_SHAPE");
|
||||||
if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION)
|
if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION)
|
||||||
fail("AUTHORING_CATALOG");
|
fail("AUTHORING_CATALOG");
|
||||||
if (
|
if (!Number.isSafeInteger(raw.version) || raw.version < 0)
|
||||||
!Number.isSafeInteger(raw.version) || raw.version < 0
|
|
||||||
)
|
|
||||||
fail("AUTHORING_SHAPE");
|
fail("AUTHORING_SHAPE");
|
||||||
if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff)
|
if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff)
|
||||||
fail("AUTHORING_REVISION");
|
fail("AUTHORING_REVISION");
|
||||||
@@ -134,7 +154,20 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
definition = nodeDefinitions[n.typeId];
|
definition = nodeDefinitions[n.typeId];
|
||||||
if (!descriptor)
|
if (!descriptor)
|
||||||
fail("AUTHORING_NODE_TYPE", { nodeId: n.id, typeId: n.typeId });
|
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 (Object.hasOwn(n, "parentId")) nodeKeys.push("parentId");
|
||||||
if (
|
if (
|
||||||
!exactKeys(n, nodeKeys) ||
|
!exactKeys(n, nodeKeys) ||
|
||||||
@@ -143,10 +176,17 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
typeof n.muted !== "boolean" ||
|
typeof n.muted !== "boolean" ||
|
||||||
typeof n.collapsed !== "boolean" ||
|
typeof n.collapsed !== "boolean" ||
|
||||||
typeof n.label !== "string" ||
|
typeof n.label !== "string" ||
|
||||||
!exactKeys(n.position, ["x", "y"]) || !Number.isFinite(n.position.x) || !Number.isFinite(n.position.y) ||
|
!exactKeys(n.position, ["x", "y"]) ||
|
||||||
!exactKeys(n.size, ["x", "y"]) || !Number.isFinite(n.size.x) || !Number.isFinite(n.size.y) || n.size.x <= 0 || n.size.y <= 0 ||
|
!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.hasOwn(n, "parentId") && !identifier(n.parentId)) ||
|
||||||
!object(n.extensions) || !finiteJson(n.extensions) ||
|
!object(n.extensions) ||
|
||||||
|
!finiteJson(n.extensions) ||
|
||||||
!Array.isArray(n.sockets) ||
|
!Array.isArray(n.sockets) ||
|
||||||
!object(n.parameters)
|
!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 = [
|
const expected = [
|
||||||
...Object.keys(descriptor.inputs),
|
...Object.keys(descriptor.inputs),
|
||||||
...Object.keys(descriptor.outputs),
|
...Object.keys(descriptor.outputs),
|
||||||
@@ -181,17 +263,49 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
socketDefinition = definition.sockets[s.key],
|
socketDefinition = definition.sockets[s.key],
|
||||||
direction = input ? "input" : "output",
|
direction = input ? "input" : "output",
|
||||||
dataType = socketDefinition.type,
|
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 (
|
if (
|
||||||
!exactKeys(s, socketKeys) ||
|
!exactKeys(s, socketKeys) ||
|
||||||
s.id !== `${n.id}:${s.key}` ||
|
s.id !== `${n.id}:${s.key}` ||
|
||||||
s.label !== socketDefinition.title ||
|
s.label !== socketDefinition.title ||
|
||||||
s.direction !== direction ||
|
s.direction !== direction ||
|
||||||
s.dataType !== dataType ||
|
s.dataType !== dataType ||
|
||||||
!Array.isArray(s.accepts) || s.accepts.length !== (direction === "input" ? socketTypes[dataType].acceptsFrom.length : 0) ||
|
!Array.isArray(s.accepts) ||
|
||||||
!s.accepts.every((v, i) => v === (direction === "input" ? socketTypes[dataType].acceptsFrom[i] : undefined)) ||
|
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
|
(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.defaultValue !== undefined) ||
|
||||||
s.visible !== socketDefinition.visible ||
|
s.visible !== socketDefinition.visible ||
|
||||||
s.maxIncomingLinks !== socketDefinition.maxIncomingLinks
|
s.maxIncomingLinks !== socketDefinition.maxIncomingLinks
|
||||||
@@ -206,10 +320,21 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
: descriptor.outputs[s.key].type,
|
: descriptor.outputs[s.key].type,
|
||||||
authoringType: s.dataType,
|
authoringType: s.dataType,
|
||||||
maxIncomingLinks: s.maxIncomingLinks,
|
maxIncomingLinks: s.maxIncomingLinks,
|
||||||
|
defaultValue: socketDefinition.value
|
||||||
|
? structuredClone(s.defaultValue)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
|
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
|
||||||
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
|
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, {
|
nodes.set(n.id, {
|
||||||
ordinal,
|
ordinal,
|
||||||
value: {
|
value: {
|
||||||
@@ -230,8 +355,18 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
!object(link) ||
|
!object(link) ||
|
||||||
!identifier(link.id) ||
|
!identifier(link.id) ||
|
||||||
linkIds.has(link.id) ||
|
linkIds.has(link.id) ||
|
||||||
!exactKeys(link, ["id", "fromNodeId", "fromSocketId", "toNodeId", "toSocketId", "muted", "extensions"]) ||
|
!exactKeys(link, [
|
||||||
typeof link.muted !== "boolean" || !object(link.extensions) || !finiteJson(link.extensions)
|
"id",
|
||||||
|
"fromNodeId",
|
||||||
|
"fromSocketId",
|
||||||
|
"toNodeId",
|
||||||
|
"toSocketId",
|
||||||
|
"muted",
|
||||||
|
"extensions",
|
||||||
|
]) ||
|
||||||
|
typeof link.muted !== "boolean" ||
|
||||||
|
!object(link.extensions) ||
|
||||||
|
!finiteJson(link.extensions)
|
||||||
)
|
)
|
||||||
fail("AUTHORING_LINK", { linkId: link?.id });
|
fail("AUTHORING_LINK", { linkId: link?.id });
|
||||||
linkIds.add(link.id);
|
linkIds.add(link.id);
|
||||||
@@ -244,21 +379,33 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
link.toNodeId !== to.node ||
|
link.toNodeId !== to.node ||
|
||||||
from.direction !== "output" ||
|
from.direction !== "output" ||
|
||||||
to.direction !== "input" ||
|
to.direction !== "input" ||
|
||||||
(!link.muted && (incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
|
(!link.muted &&
|
||||||
|
(incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
|
||||||
)
|
)
|
||||||
fail(
|
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_INCOMING"
|
||||||
: "AUTHORING_LINK",
|
: "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 }
|
? { socketId: link.toSocketId }
|
||||||
: { linkId: link.id },
|
: { linkId: link.id },
|
||||||
);
|
);
|
||||||
const accepted =
|
const accepted =
|
||||||
descriptors[nodes.get(to.node).value.executor.key].inputs[to.key]
|
descriptors[nodes.get(to.node).value.executor.key].inputs[to.key]
|
||||||
.accepted.types;
|
.accepted.types;
|
||||||
const authoringAccepted = socketTypes[nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key].type].acceptsFrom;
|
const authoringAccepted =
|
||||||
if (!accepted.includes(from.semanticType) || !authoringAccepted.includes(from.authoringType))
|
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 });
|
fail("AUTHORING_LINK_TYPE", { linkId: link.id });
|
||||||
const linkSource = {
|
const linkSource = {
|
||||||
kind: "link",
|
kind: "link",
|
||||||
@@ -299,13 +446,92 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
|||||||
const base = `nodes[${wireOrdinal}]`;
|
const base = `nodes[${wireOrdinal}]`;
|
||||||
const nodeSource = { kind: "node", nodeId: item.value.id };
|
const nodeSource = { kind: "node", nodeId: item.value.id };
|
||||||
paths[base] = nodeSource;
|
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}.${field}`] = nodeSource;
|
||||||
paths[`${base}.parameters`] = nodeSource;
|
paths[`${base}.parameters`] = nodeSource;
|
||||||
|
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))
|
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]);
|
mapValuePaths(
|
||||||
for (const key of Object.keys(descriptors[item.value.executor.key].inputs)) {
|
paths,
|
||||||
const link = raw.links.find((x) => !x.muted && x.toNodeId === item.value.id && sockets.get(x.toSocketId)?.key === key);
|
`${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) ?? {
|
const source = linkSources.get(link?.id) ?? {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
nodeId: item.value.id,
|
nodeId: item.value.id,
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { semanticCatalog } from "./catalog.js";
|
|||||||
const GROUPS = Object.freeze([
|
const GROUPS = Object.freeze([
|
||||||
["source", "Source"],
|
["source", "Source"],
|
||||||
["compute", "Compute"],
|
["compute", "Compute"],
|
||||||
|
["cpu_preparation", "CPU preparation"],
|
||||||
["render", "Render / post"],
|
["render", "Render / post"],
|
||||||
["present", "Present"],
|
["frame", "Frame"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const title = (typeId) => typeId.replaceAll("_", " ");
|
const title = (typeId) => typeId.replaceAll("_", " ");
|
||||||
|
|||||||
+186
-97
@@ -1,7 +1,6 @@
|
|||||||
export const GRAPH_ID = "authored_gpu_culling";
|
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 exact = (type) => ({ kind: "exact", types: [type] });
|
||||||
const oneOf = (...types) => ({ kind: "one_of", types });
|
|
||||||
const i = (type, required = true, authoringType) => ({
|
const i = (type, required = true, authoringType) => ({
|
||||||
accepted: typeof type === "string" ? exact(type) : type,
|
accepted: typeof type === "string" ? exact(type) : type,
|
||||||
required,
|
required,
|
||||||
@@ -25,104 +24,91 @@ const texture = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const semanticCatalog = Object.freeze({
|
export const semanticCatalog = Object.freeze({
|
||||||
surface_target: {
|
mesh: {
|
||||||
execution: "source",
|
execution: "source",
|
||||||
inputs: {},
|
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: {
|
outputs: {
|
||||||
flags: {
|
mesh: o("mesh_data"),
|
||||||
|
localAabbs: o("local_aabb_buffer"),
|
||||||
|
isVisible: {
|
||||||
...o("boolean_flag_buffer"),
|
...o("boolean_flag_buffer"),
|
||||||
authoringType: "visibility_flag_buffer",
|
authoringType: "visibility_flag_buffer",
|
||||||
},
|
},
|
||||||
|
pipelineIndices: o("pipeline_index_stream"),
|
||||||
},
|
},
|
||||||
parameters: {},
|
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: {
|
frustum_cull: {
|
||||||
execution: "compute",
|
execution: "compute",
|
||||||
inputs: {
|
inputs: {
|
||||||
scene: i("scene_table"),
|
mesh: i("mesh_data"),
|
||||||
localAabbs: i("local_aabb_buffer"),
|
localAabbs: i("local_aabb_buffer"),
|
||||||
frustum: i("camera_frustum"),
|
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
flags: {
|
isFrustumCulled: {
|
||||||
...o("boolean_flag_buffer"),
|
...o("boolean_flag_buffer"),
|
||||||
authoringType: "frustum_flag_buffer",
|
authoringType: "frustum_flag_buffer",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
parameters: {},
|
parameters: { cameraSelection: "active" },
|
||||||
},
|
},
|
||||||
mesh_query: {
|
mesh_query: {
|
||||||
execution: "compute",
|
execution: "compute",
|
||||||
inputs: {
|
inputs: {
|
||||||
scene: i("scene_table"),
|
mesh: i("mesh_data"),
|
||||||
isVisible: i("boolean_flag_buffer", false, "visibility_flag_buffer"),
|
isVisible: i("boolean_flag_buffer", false, "visibility_flag_buffer"),
|
||||||
isFrustumCulled: i("boolean_flag_buffer", false, "frustum_flag_buffer"),
|
isFrustumCulled: i("boolean_flag_buffer", false, "frustum_flag_buffer"),
|
||||||
},
|
},
|
||||||
outputs: { draws: o("draw_stream") },
|
outputs: { draws: o("draw_stream") },
|
||||||
parameters: {
|
parameters: {
|
||||||
filters: [
|
visiblePredicate: "required_true",
|
||||||
{ flag: "isVisible", predicate: "required_true" },
|
frustumCulledPredicate: "required_false",
|
||||||
{ flag: "isFrustumCulled", predicate: "required_false" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
depth_stencil_config: {
|
pipeline_registry: {
|
||||||
execution: "source",
|
execution: "cpu_preparation",
|
||||||
inputs: {},
|
inputs: { pipelineIndices: i("pipeline_index_stream") },
|
||||||
outputs: { config: o("depth_stencil_config") },
|
outputs: { activation: o("pipeline_activation") },
|
||||||
parameters: {
|
parameters: {},
|
||||||
depthCompare: "less_equal",
|
|
||||||
depthWriteEnabled: true,
|
|
||||||
clearDepth: 1,
|
|
||||||
},
|
},
|
||||||
},
|
pipeline: {
|
||||||
legacy_forward: {
|
|
||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
scene: i("scene_table"),
|
mesh: i("mesh_data"),
|
||||||
draws: i("draw_stream"),
|
draws: i("draw_stream"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
activation: i("pipeline_activation"),
|
||||||
depthTarget: i(oneOf("texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
depthStencil: i("depth_stencil_config"),
|
depthTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture"), depth: o("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: {
|
fullscreen_copy: {
|
||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
source: i("texture"),
|
source: i("texture"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: {},
|
parameters: {},
|
||||||
@@ -131,7 +117,7 @@ export const semanticCatalog = Object.freeze({
|
|||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
source: i("texture"),
|
source: i("texture"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: { exposure: 1 },
|
parameters: { exposure: 1 },
|
||||||
@@ -140,7 +126,7 @@ export const semanticCatalog = Object.freeze({
|
|||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
source: i("texture"),
|
source: i("texture"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: { threshold: 1, knee: 0.5 },
|
parameters: { threshold: 1, knee: 0.5 },
|
||||||
@@ -149,7 +135,7 @@ export const semanticCatalog = Object.freeze({
|
|||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
source: i("texture"),
|
source: i("texture"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: { direction: [1, 0], radius: 1 },
|
parameters: { direction: [1, 0], radius: 1 },
|
||||||
@@ -159,7 +145,7 @@ export const semanticCatalog = Object.freeze({
|
|||||||
inputs: {
|
inputs: {
|
||||||
source: i("texture"),
|
source: i("texture"),
|
||||||
bloom: i("texture"),
|
bloom: i("texture"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: { intensity: 1 },
|
parameters: { intensity: 1 },
|
||||||
@@ -168,14 +154,14 @@ export const semanticCatalog = Object.freeze({
|
|||||||
execution: "render",
|
execution: "render",
|
||||||
inputs: {
|
inputs: {
|
||||||
source: i("texture"),
|
source: i("texture"),
|
||||||
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
|
colorTarget: i("texture"),
|
||||||
},
|
},
|
||||||
outputs: { color: o("texture") },
|
outputs: { color: o("texture") },
|
||||||
parameters: { strength: 2 },
|
parameters: { strength: 2 },
|
||||||
},
|
},
|
||||||
present: {
|
frame_out: {
|
||||||
execution: "present",
|
execution: "frame",
|
||||||
inputs: { surface: i("texture") },
|
inputs: { color: i("texture") },
|
||||||
outputs: {},
|
outputs: {},
|
||||||
parameters: {},
|
parameters: {},
|
||||||
},
|
},
|
||||||
@@ -193,15 +179,13 @@ const socketColors = [
|
|||||||
];
|
];
|
||||||
export const socketTypes = Object.fromEntries(
|
export const socketTypes = Object.fromEntries(
|
||||||
[
|
[
|
||||||
"surface_target",
|
|
||||||
"texture_spec",
|
|
||||||
"texture",
|
"texture",
|
||||||
"scene_table",
|
"mesh_data",
|
||||||
"local_aabb_buffer",
|
"local_aabb_buffer",
|
||||||
"camera_frustum",
|
|
||||||
"boolean_flag_buffer",
|
"boolean_flag_buffer",
|
||||||
|
"pipeline_index_stream",
|
||||||
"draw_stream",
|
"draw_stream",
|
||||||
"depth_stencil_config",
|
"pipeline_activation",
|
||||||
"visibility_flag_buffer",
|
"visibility_flag_buffer",
|
||||||
"frustum_flag_buffer",
|
"frustum_flag_buffer",
|
||||||
].map((type, index) => [
|
].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 = [
|
socketTypes.boolean_flag_buffer.acceptsFrom = [
|
||||||
"boolean_flag_buffer",
|
"boolean_flag_buffer",
|
||||||
"visibility_flag_buffer",
|
"visibility_flag_buffer",
|
||||||
@@ -259,33 +237,138 @@ export const theme = {
|
|||||||
export const styles = {
|
export const styles = {
|
||||||
source: { header: "#3977a8" },
|
source: { header: "#3977a8" },
|
||||||
compute: { header: "#725a9b" },
|
compute: { header: "#725a9b" },
|
||||||
|
cpu_preparation: { header: "#8a6d3b" },
|
||||||
render: { header: "#426b43" },
|
render: { header: "#426b43" },
|
||||||
present: { header: "#a75d37" },
|
frame: { header: "#a75d37" },
|
||||||
};
|
};
|
||||||
const socket = (title, direction, type) => ({
|
const socket = (title, direction, type, value = null) => ({
|
||||||
title,
|
title,
|
||||||
direction,
|
direction,
|
||||||
type,
|
type,
|
||||||
maxIncomingLinks: direction === "input" ? 1 : 0,
|
maxIncomingLinks: direction === "input" ? 1 : 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
value: null,
|
value,
|
||||||
showValue: false,
|
showValue: value !== null,
|
||||||
});
|
});
|
||||||
const parameterSchema = (value) =>
|
const tagged = (kind, value) => ({ kind, value: structuredClone(value) });
|
||||||
typeof value === "number"
|
const number = (value, minimum, maximum) => ({
|
||||||
? { type: "number", default: { kind: "number", value } }
|
type: "number",
|
||||||
: typeof value === "string"
|
default: tagged("number", value),
|
||||||
? { type: "string", default: { kind: "string", value } }
|
minimum,
|
||||||
: typeof value === "boolean"
|
maximum,
|
||||||
? { type: "boolean", default: { kind: "boolean", value } }
|
});
|
||||||
: { type: "json", default: { kind: "json", value } };
|
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(
|
export const nodeDefinitions = Object.fromEntries(
|
||||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||||
const sockets = {
|
const sockets = {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Object.entries(c.inputs).map(([n, v]) => [
|
Object.entries(c.inputs).map(([n, v]) => [
|
||||||
n,
|
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(
|
...Object.fromEntries(
|
||||||
@@ -295,12 +378,15 @@ export const nodeDefinitions = Object.fromEntries(
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
parameters = Object.fromEntries(
|
parameters = parameterSchemas[key];
|
||||||
Object.entries(c.parameters).map(([name, value]) => [
|
if (
|
||||||
name,
|
!parameters ||
|
||||||
parameterSchema(value),
|
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 [
|
return [
|
||||||
key,
|
key,
|
||||||
{
|
{
|
||||||
@@ -314,6 +400,9 @@ export const nodeDefinitions = Object.fromEntries(
|
|||||||
...Object.keys(parameters).map((parameter) => ({
|
...Object.keys(parameters).map((parameter) => ({
|
||||||
kind: "parameter",
|
kind: "parameter",
|
||||||
parameter,
|
parameter,
|
||||||
|
...(key === "frustum_cull" && parameter === "cameraSelection"
|
||||||
|
? { title: "Camera" }
|
||||||
|
: {}),
|
||||||
})),
|
})),
|
||||||
...Object.keys(sockets).map((socket) => ({ kind: "socket", socket })),
|
...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";
|
import { createNodeIdAllocator, spawnRequestedNode } from "./node-spawn.js";
|
||||||
|
|
||||||
const spec = [
|
const spec = [
|
||||||
["surface", "surface_target", { x: 40, y: 40 }],
|
["hdr", "texture", { x: 40, y: 170 }],
|
||||||
["hdr", "texture_spec", { x: 40, y: 170 }],
|
["depth", "texture", { x: 40, y: 300 }],
|
||||||
["depth", "texture_spec", { x: 40, y: 300 }],
|
["mesh", "mesh", { x: 40, y: 470 }],
|
||||||
["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 }],
|
|
||||||
["cull", "frustum_cull", { x: 540, y: 480 }],
|
["cull", "frustum_cull", { x: 540, y: 480 }],
|
||||||
["query", "mesh_query", { x: 790, y: 330 }],
|
["query", "mesh_query", { x: 790, y: 330 }],
|
||||||
["depth_config", "depth_stencil_config", { x: 790, y: 620 }],
|
["registry", "pipeline_registry", { x: 790, y: 620 }],
|
||||||
["forward", "legacy_forward", { x: 1040, y: 290 }],
|
["ground", "pipeline", { x: 1040, y: 290 }],
|
||||||
["copy", "fullscreen_copy", { x: 1300, y: 250 }],
|
["pbr", "pipeline", { x: 1300, y: 290 }],
|
||||||
["present", "present", { x: 1540, y: 250 }],
|
["pbr_double", "pipeline", { x: 1560, y: 290 }],
|
||||||
|
["frame_out", "frame_out", { x: 1820, y: 250 }],
|
||||||
];
|
];
|
||||||
async function seed(root) {
|
async function seed(root) {
|
||||||
await root.setState({
|
await root.setState({
|
||||||
@@ -30,22 +27,24 @@ async function seed(root) {
|
|||||||
for (const [nodeId, nodeType, position] of spec)
|
for (const [nodeId, nodeType, position] of spec)
|
||||||
await root.dispatch({ type: "node.add", nodeId, nodeType, position });
|
await root.dispatch({ type: "node.add", nodeId, nodeType, position });
|
||||||
const links = [
|
const links = [
|
||||||
["scene", "scene", "aabbs", "scene"],
|
["mesh", "mesh", "cull", "mesh"],
|
||||||
["scene", "scene", "visible", "scene"],
|
["mesh", "localAabbs", "cull", "localAabbs"],
|
||||||
["scene", "scene", "cull", "scene"],
|
["mesh", "mesh", "query", "mesh"],
|
||||||
["aabbs", "localAabbs", "cull", "localAabbs"],
|
["mesh", "isVisible", "query", "isVisible"],
|
||||||
["frustum", "frustum", "cull", "frustum"],
|
["cull", "isFrustumCulled", "query", "isFrustumCulled"],
|
||||||
["scene", "scene", "query", "scene"],
|
["mesh", "pipelineIndices", "registry", "pipelineIndices"],
|
||||||
["visible", "flags", "query", "isVisible"],
|
...["ground", "pbr", "pbr_double"].flatMap((pipeline) => [
|
||||||
["cull", "flags", "query", "isFrustumCulled"],
|
["mesh", "mesh", pipeline, "mesh"],
|
||||||
["scene", "scene", "forward", "scene"],
|
["query", "draws", pipeline, "draws"],
|
||||||
["query", "draws", "forward", "draws"],
|
["registry", "activation", pipeline, "activation"],
|
||||||
["hdr", "spec", "forward", "colorTarget"],
|
]),
|
||||||
["depth", "spec", "forward", "depthTarget"],
|
["hdr", "texture", "ground", "colorTarget"],
|
||||||
["depth_config", "config", "forward", "depthStencil"],
|
["depth", "texture", "ground", "depthTarget"],
|
||||||
["forward", "color", "copy", "source"],
|
["ground", "color", "pbr", "colorTarget"],
|
||||||
["surface", "surface", "copy", "colorTarget"],
|
["ground", "depth", "pbr", "depthTarget"],
|
||||||
["copy", "color", "present", "surface"],
|
["pbr", "color", "pbr_double", "colorTarget"],
|
||||||
|
["pbr", "depth", "pbr_double", "depthTarget"],
|
||||||
|
["pbr_double", "color", "frame_out", "color"],
|
||||||
];
|
];
|
||||||
for (const [a, as, b, bs] of links) {
|
for (const [a, as, b, bs] of links) {
|
||||||
const id = `${a}_${as}_${b}_${bs}`;
|
const id = `${a}_${as}_${b}_${bs}`;
|
||||||
@@ -64,34 +63,44 @@ async function seed(root) {
|
|||||||
}
|
}
|
||||||
const authored = await root.getState(),
|
const authored = await root.getState(),
|
||||||
depth = authored.nodes.find((node) => node.id === "depth");
|
depth = authored.nodes.find((node) => node.id === "depth");
|
||||||
depth.parameters.texture = {
|
depth.parameters.format = { kind: "string", value: "depth32_float" };
|
||||||
kind: "json",
|
for (const [id, name] of [["ground", "ground_plane"], ["pbr", "gltf_standard"], ["pbr_double", "gltf_standard_double_sided"]])
|
||||||
value: {
|
authored.nodes.find((node) => node.id === id).parameters.pipeline = { kind: "string", value: name };
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await root.setState(authored);
|
await root.setState(authored);
|
||||||
}
|
}
|
||||||
export async function createRenderGraphEditor(canvas) {
|
export async function createRenderGraphEditor(canvas) {
|
||||||
const allocateId = createNodeIdAllocator();
|
const allocateId = createNodeIdAllocator();
|
||||||
let root, view, menu, destroying, dead = false;
|
let root,
|
||||||
const requestAddNode = Object.assign(async (request, point, isCurrent = () => true) => {
|
view,
|
||||||
|
menu,
|
||||||
|
destroying,
|
||||||
|
dead = false;
|
||||||
|
const requestAddNode = Object.assign(
|
||||||
|
async (request, point, isCurrent = () => true) => {
|
||||||
let typeId;
|
let typeId;
|
||||||
try { typeId = await menu?.open(point); } catch (error) { if (!dead && isCurrent()) console.error(error); return; }
|
try {
|
||||||
|
typeId = await menu?.open(point);
|
||||||
|
} catch (error) {
|
||||||
|
if (!dead && isCurrent()) console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (dead || !isCurrent() || !root || !view) return;
|
if (dead || !isCurrent() || !root || !view) return;
|
||||||
const alive = () => !dead && isCurrent();
|
const alive = () => !dead && isCurrent();
|
||||||
try { await spawnRequestedNode(root, view, request, typeId, allocateId, alive); } catch (error) { if (!dead) console.error(error); }
|
try {
|
||||||
}, { close: () => menu?.close() });
|
await spawnRequestedNode(
|
||||||
|
root,
|
||||||
|
view,
|
||||||
|
request,
|
||||||
|
typeId,
|
||||||
|
allocateId,
|
||||||
|
alive,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!dead) console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ close: () => menu?.close() },
|
||||||
|
);
|
||||||
const host = prepareBrowserHost(canvas, { requestAddNode });
|
const host = prepareBrowserHost(canvas, { requestAddNode });
|
||||||
const destroy = () =>
|
const destroy = () =>
|
||||||
(destroying ??= (async () => {
|
(destroying ??= (async () => {
|
||||||
|
|||||||
+56
-258
@@ -1,278 +1,76 @@
|
|||||||
const input = (node, socket) => ({ node, socket });
|
const input = (node, socket) => ({ node, socket });
|
||||||
const node = (id, key, parameters = {}, inputs = {}) => ({
|
const node = (id, key, parameters = {}, inputs = {}) => ({
|
||||||
id,
|
id, state: "enabled", executor: { key, version: 1 }, parameters, inputs,
|
||||||
state: "enabled",
|
|
||||||
executor: { key, version: 1 },
|
|
||||||
parameters,
|
|
||||||
inputs,
|
|
||||||
});
|
});
|
||||||
const texture = (format) => ({
|
const texture = (format, scale = 1) => ({
|
||||||
texture: {
|
texture: {
|
||||||
dimension: "d2",
|
dimension: "d2", format,
|
||||||
format,
|
extent: { kind: "surface_relative", width: { numerator: 1, denominator: scale }, height: { numerator: 1, denominator: scale }, depthOrArrayLayers: 1 },
|
||||||
extent: {
|
mipLevelCount: 1, sampleCount: 1, viewFormats: [],
|
||||||
kind: "surface_relative",
|
|
||||||
width: { numerator: 1, denominator: 1 },
|
|
||||||
height: { numerator: 1, denominator: 1 },
|
|
||||||
depthOrArrayLayers: 1,
|
|
||||||
},
|
|
||||||
mipLevelCount: 1,
|
|
||||||
sampleCount: 1,
|
|
||||||
viewFormats: [],
|
|
||||||
},
|
},
|
||||||
residency: "transient",
|
residency: "transient",
|
||||||
});
|
});
|
||||||
const direct = (graphId, clearColor) => Object.freeze({
|
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1]) => [
|
||||||
schemaVersion: 2,
|
node("hdr", "texture", texture("rgba16_float")),
|
||||||
graphId,
|
node("depth", "texture", texture("depth32_float")),
|
||||||
revision: 1,
|
node("mesh", "mesh"),
|
||||||
nodes: [
|
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
|
||||||
node("surface", "surface_target"),
|
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
|
||||||
node("depth", "texture_spec", texture("depth32_float")),
|
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("scene", "scene_table"),
|
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("visible", "visibility_flags", {}, { scene: input("scene", "scene") }),
|
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") }),
|
||||||
node("query", "mesh_query", {
|
];
|
||||||
filters: [
|
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
||||||
{ flag: "isVisible", predicate: "required_true" },
|
const direct = (graphId, clearColor) => graph(graphId, [
|
||||||
{ flag: "isFrustumCulled", predicate: "any" },
|
node("ldr", "texture", texture("rgba8_unorm")),
|
||||||
],
|
...scene("ldr", clearColor).filter((item) => item.id !== "hdr"),
|
||||||
}, { scene: input("scene", "scene"), isVisible: input("visible", "flags") }),
|
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
|
||||||
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") }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
|
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
|
||||||
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
|
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
|
||||||
export const hdr = Object.freeze({
|
export const hdr = graph("preset_hdr_fullscreen", [
|
||||||
schemaVersion: 2,
|
...scene("hdr"),
|
||||||
graphId: "preset_hdr_fullscreen",
|
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
|
||||||
revision: 1,
|
]);
|
||||||
nodes: [
|
export const culling = graph("preset_gpu_culling", (() => {
|
||||||
node("surface", "surface_target"),
|
const nodes = structuredClone(hdr.nodes);
|
||||||
node("hdr", "texture_spec", texture("rgba16_float")),
|
nodes.splice(3, 0, node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabbs: input("mesh", "localAabbs") }));
|
||||||
node("depth", "texture_spec", texture("depth32_float")),
|
const query = nodes.find((item) => item.id === "query");
|
||||||
node("scene", "scene_table"),
|
query.parameters.frustumCulledPredicate = "required_false";
|
||||||
node("visible", "visibility_flags", {}, { scene: input("scene", "scene") }),
|
query.inputs.isFrustumCulled = input("cull", "isFrustumCulled");
|
||||||
node(
|
return nodes;
|
||||||
"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;
|
|
||||||
})());
|
})());
|
||||||
const postPreset = (graphId, kind) => {
|
const postPreset = (graphId, kind) => {
|
||||||
const nodes = hdr.nodes.slice(0, 8).map((x) => structuredClone(x));
|
const nodes = [node("ldr", "texture", texture("rgba8_unorm")), ...scene("hdr")];
|
||||||
if (kind === "tone")
|
let source = "pbr_double";
|
||||||
nodes.push(
|
|
||||||
node(
|
|
||||||
"tone",
|
|
||||||
"tone_map",
|
|
||||||
{ exposure: 1 },
|
|
||||||
{
|
|
||||||
source: input("forward", "color"),
|
|
||||||
colorTarget: input("surface", "surface"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (kind === "edges") {
|
if (kind === "edges") {
|
||||||
nodes.splice(1, 0, node("edge_hdr", "texture_spec", texture("rgba16_float")));
|
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||||
nodes.push(
|
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
||||||
node(
|
source = "edges";
|
||||||
"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"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (kind === "bloom" || kind === "combined") {
|
if (kind === "bloom" || kind === "combined") {
|
||||||
const half = {
|
nodes.splice(1, 0,
|
||||||
texture: {
|
node("half_a", "texture", texture("rgba16_float", 2)), node("half_b", "texture", texture("rgba16_float", 2)),
|
||||||
...texture("rgba16_float").texture,
|
node("half_c", "texture", texture("rgba16_float", 2)), node("composite_hdr", "texture", texture("rgba16_float")));
|
||||||
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.push(
|
nodes.push(
|
||||||
node(
|
node("extract", "bloom_extract", { threshold: 1, knee: 0.5 }, { source: input("pbr_double", "color"), colorTarget: input("half_a", "texture") }),
|
||||||
"extract",
|
node("blur_h", "bloom_blur", { direction: [1, 0], radius: 1 }, { source: input("extract", "color"), colorTarget: input("half_b", "texture") }),
|
||||||
"bloom_extract",
|
node("blur_v", "bloom_blur", { direction: [0, 1], radius: 1 }, { source: input("blur_h", "color"), colorTarget: input("half_c", "texture") }),
|
||||||
{ threshold: 1, knee: 0.5 },
|
node("composite", "bloom_composite", { intensity: 0.8 }, { source: input("pbr_double", "color"), bloom: input("blur_v", "color"), colorTarget: input("composite_hdr", "texture") }),
|
||||||
{
|
|
||||||
source: input("forward", "color"),
|
|
||||||
colorTarget: input("half_a", "spec"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
nodes.push(
|
source = "composite";
|
||||||
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";
|
|
||||||
if (kind === "combined") {
|
if (kind === "combined") {
|
||||||
nodes.splice(1, 0, node("edge_hdr", "texture_spec", texture("rgba16_float")));
|
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
|
||||||
nodes.push(
|
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
|
||||||
node(
|
source = "edges";
|
||||||
"edges",
|
|
||||||
"luminance_edge",
|
|
||||||
{ strength: 2 },
|
|
||||||
{
|
|
||||||
source: input("composite", "color"),
|
|
||||||
colorTarget: input("edge_hdr", "spec"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
toneSource = "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("tone", "tone_map", { exposure: 1 }, { source: input(source, "color"), colorTarget: input("ldr", "texture") }));
|
||||||
nodes.push(
|
nodes.push(node("frame_out", "frame_out", {}, { color: input("tone", "color") }));
|
||||||
node("present", "present", {}, { surface: input(last.id, "color") }),
|
return graph(graphId, nodes);
|
||||||
);
|
|
||||||
return Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
|
||||||
};
|
};
|
||||||
export const tone = postPreset("preset_tone", "tone"),
|
export const tone = postPreset("preset_tone", "tone");
|
||||||
edges = postPreset("preset_edges", "edges"),
|
export const edges = postPreset("preset_edges", "edges");
|
||||||
bloom = postPreset("preset_bloom", "bloom"),
|
export const bloom = postPreset("preset_bloom", "bloom");
|
||||||
combined = postPreset("preset_combined", "combined");
|
export const combined = postPreset("preset_combined", "combined");
|
||||||
export const renderGraphPresets = Object.freeze({
|
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, edges, bloom, combined });
|
||||||
midnight,
|
|
||||||
ember,
|
|
||||||
hdr,
|
|
||||||
culling,
|
|
||||||
tone,
|
|
||||||
edges,
|
|
||||||
bloom,
|
|
||||||
combined,
|
|
||||||
});
|
|
||||||
|
|||||||
+154
-34
@@ -1,13 +1,26 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { addNodeItems, moveAddNodeSelection, searchAddNodeItems } from "../static/render-graph/add-node-menu.js";
|
import {
|
||||||
import { createNodeIdAllocator, spawnRequestedNode } from "../static/render-graph/node-spawn.js";
|
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", () => {
|
test("add-node model contains all 13 catalog types in application groups", () => {
|
||||||
assert.equal(addNodeItems.length, 17);
|
assert.equal(addNodeItems.length, 13);
|
||||||
assert.deepEqual([...new Set(addNodeItems.map((item) => item.group))], ["Source", "Compute", "Render / post", "Present"]);
|
assert.deepEqual(
|
||||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 17);
|
[...new Set(addNodeItems.map((item) => item.group))],
|
||||||
assert.deepEqual(searchAddNodeItems("tone render").map((item) => item.typeId), ["tone_map"]);
|
["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"), []);
|
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 values = ["a-a", "a-a", "b-b"];
|
||||||
const allocate = createNodeIdAllocator(() => values.shift());
|
const allocate = createNodeIdAllocator(() => values.shift());
|
||||||
assert.equal(allocate(["node_aa"]), "node_bb");
|
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 () => {
|
test("all 13 types spawn with exact position, current version and generated ID", async () => {
|
||||||
let revision = 5, expectedType;
|
let revision = 5,
|
||||||
|
expectedType;
|
||||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
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 = {
|
const view = {
|
||||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||||
addNode: async (params, options) => {
|
addNode: async (params, options) => {
|
||||||
@@ -38,41 +57,89 @@ test("all 17 types spawn with exact position, current version and generated ID",
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
let id = 0;
|
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) {
|
for (const item of addNodeItems) {
|
||||||
expectedType = item.typeId;
|
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;
|
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 () => {
|
test("spawn rechecks composition after getState and propagates add errors", async () => {
|
||||||
let revision = 2;
|
let revision = 2;
|
||||||
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 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 allocate = createNodeIdAllocator(() => "a");
|
||||||
const view = { getHostSnapshot: () => ({ compositionRevision: revision }), addNode: async () => { throw Error("must not add"); } };
|
const view = {
|
||||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", allocate), false);
|
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||||
|
addNode: async () => {
|
||||||
|
throw Error("must not add");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
await spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||||
|
false,
|
||||||
|
);
|
||||||
revision = 2;
|
revision = 2;
|
||||||
root.getState = async () => ({ version: 3, nodes: [] });
|
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 () => {
|
test("spawn cancels when a pending getState becomes mutated or dead", async () => {
|
||||||
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 2 } };
|
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 2 } };
|
||||||
let resolveState, revision = 2, alive = true, adds = 0;
|
let resolveState,
|
||||||
const root = { getState: () => new Promise((resolve) => { resolveState = resolve; }) };
|
revision = 2,
|
||||||
|
alive = true,
|
||||||
|
adds = 0;
|
||||||
|
const root = {
|
||||||
|
getState: () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
resolveState = resolve;
|
||||||
|
}),
|
||||||
|
};
|
||||||
const view = {
|
const view = {
|
||||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
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++;
|
revision++;
|
||||||
resolveState({ version: 1, nodes: [] });
|
resolveState({ version: 1, nodes: [] });
|
||||||
assert.equal(await pendingMutation, false);
|
assert.equal(await pendingMutation, false);
|
||||||
revision = 2;
|
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;
|
alive = false;
|
||||||
resolveState({ version: 1, nodes: [] });
|
resolveState({ version: 1, nodes: [] });
|
||||||
assert.equal(await pendingDestroy, false);
|
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 () => {
|
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 request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
||||||
const root = { getState: async () => ({ version: 1, nodes: [] }) };
|
const root = { getState: async () => ({ version: 1, nodes: [] }) };
|
||||||
const view = { getHostSnapshot: () => ({ compositionRevision: 1 }), addNode: async () => { adds++; } };
|
const view = {
|
||||||
const result = await spawnRequestedNode(root, view, request, "tone_map", () => {
|
getHostSnapshot: () => ({ compositionRevision: 1 }),
|
||||||
|
addNode: async () => {
|
||||||
|
adds++;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await spawnRequestedNode(
|
||||||
|
root,
|
||||||
|
view,
|
||||||
|
request,
|
||||||
|
"tone_map",
|
||||||
|
() => {
|
||||||
alive = false;
|
alive = false;
|
||||||
return "node_reserved";
|
return "node_reserved";
|
||||||
}, () => alive);
|
},
|
||||||
|
() => alive,
|
||||||
|
);
|
||||||
assert.equal(result, false);
|
assert.equal(result, false);
|
||||||
assert.equal(adds, 0);
|
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 () => {
|
test("spawn suppresses teardown RPC rejections but propagates genuine live add errors", async () => {
|
||||||
const request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
const request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
||||||
let alive = true;
|
let alive = true;
|
||||||
const view = { getHostSnapshot: () => ({ compositionRevision: 1 }), addNode: async () => {} };
|
const view = {
|
||||||
const root = { getState: async () => { alive = false; throw Error("detached state"); } };
|
getHostSnapshot: () => ({ compositionRevision: 1 }),
|
||||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", () => "node_a", () => alive), false);
|
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;
|
alive = true;
|
||||||
root.getState = async () => ({ version: 1, nodes: [] });
|
root.getState = async () => ({ version: 1, nodes: [] });
|
||||||
view.addNode = async () => { alive = false; throw Error("detached add"); };
|
view.addNode = async () => {
|
||||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", () => "node_b", () => alive), false);
|
alive = false;
|
||||||
|
throw Error("detached add");
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
await spawnRequestedNode(
|
||||||
|
root,
|
||||||
|
view,
|
||||||
|
request,
|
||||||
|
"tone_map",
|
||||||
|
() => "node_b",
|
||||||
|
() => alive,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
alive = true;
|
alive = true;
|
||||||
view.addNode = async () => { throw Error("live add failure"); };
|
view.addNode = async () => {
|
||||||
|
throw Error("live add failure");
|
||||||
|
};
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
spawnRequestedNode(root, view, request, "tone_map", () => "node_c", () => alive),
|
spawnRequestedNode(
|
||||||
|
root,
|
||||||
|
view,
|
||||||
|
request,
|
||||||
|
"tone_map",
|
||||||
|
() => "node_c",
|
||||||
|
() => alive,
|
||||||
|
),
|
||||||
/live add failure/,
|
/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("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("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("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("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/)});
|
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";
|
import { fxNodeComposition } from "../static/render-graph/catalog.js";
|
||||||
|
|
||||||
test("production render graph composition passes fxnode's public validator", async () => {
|
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 {
|
try {
|
||||||
const entry = path.join(directory, "entry.js");
|
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({
|
await build({
|
||||||
configFile: false,
|
configFile: false,
|
||||||
logLevel: "silent",
|
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);
|
const result = validateFxNodeComposition(fxNodeComposition);
|
||||||
assert.equal(result.ok, true, result.ok ? undefined : JSON.stringify(result.issues, null, 2));
|
assert.equal(
|
||||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 17);
|
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 {
|
} finally {
|
||||||
await rm(directory, { recursive: true, force: true });
|
await rm(directory, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,16 +41,23 @@ function fixture() {
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
sockets: [
|
sockets: [
|
||||||
...Object.entries(d.inputs).map(([key, x]) => ({
|
...Object.entries(d.inputs).map(([key, x]) => {
|
||||||
|
const socket = definition.sockets[key];
|
||||||
|
return {
|
||||||
key,
|
key,
|
||||||
id: `${n.id}:${key}`,
|
id: `${n.id}:${key}`,
|
||||||
direction: "input",
|
direction: "input",
|
||||||
dataType: x.authoringType ?? x.accepted.types[0],
|
dataType: x.authoringType ?? x.accepted.types[0],
|
||||||
label: key,
|
label: socket.title,
|
||||||
accepts: socketTypes[x.authoringType ?? x.accepted.types[0]].acceptsFrom,
|
accepts:
|
||||||
visible: true,
|
socketTypes[x.authoringType ?? x.accepted.types[0]].acceptsFrom,
|
||||||
maxIncomingLinks: 1,
|
...(socket.value
|
||||||
})),
|
? { defaultValue: structuredClone(socket.value.default) }
|
||||||
|
: {}),
|
||||||
|
visible: socket.visible,
|
||||||
|
maxIncomingLinks: socket.maxIncomingLinks,
|
||||||
|
};
|
||||||
|
}),
|
||||||
...Object.entries(d.outputs).map(([key]) => ({
|
...Object.entries(d.outputs).map(([key]) => ({
|
||||||
key,
|
key,
|
||||||
id: `${n.id}:${key}`,
|
id: `${n.id}:${key}`,
|
||||||
@@ -87,26 +94,22 @@ function fixture() {
|
|||||||
}
|
}
|
||||||
test("catalog exhaustively mirrors all current contracts", () => {
|
test("catalog exhaustively mirrors all current contracts", () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
Object.keys(semanticCatalog).sort(),
|
Object.keys(semanticCatalog),
|
||||||
[
|
[
|
||||||
"surface_target",
|
"mesh",
|
||||||
"texture_spec",
|
"texture",
|
||||||
"scene_table",
|
|
||||||
"local_aabb_buffer",
|
|
||||||
"camera_frustum",
|
|
||||||
"visibility_flags",
|
|
||||||
"frustum_cull",
|
"frustum_cull",
|
||||||
"mesh_query",
|
"mesh_query",
|
||||||
"depth_stencil_config",
|
"pipeline_registry",
|
||||||
"legacy_forward",
|
"pipeline",
|
||||||
"fullscreen_copy",
|
"fullscreen_copy",
|
||||||
"tone_map",
|
"tone_map",
|
||||||
"bloom_extract",
|
"bloom_extract",
|
||||||
"bloom_blur",
|
"bloom_blur",
|
||||||
"bloom_composite",
|
"bloom_composite",
|
||||||
"luminance_edge",
|
"luminance_edge",
|
||||||
"present",
|
"frame_out",
|
||||||
].sort(),
|
],
|
||||||
);
|
);
|
||||||
for (const c of Object.values(semanticCatalog)) {
|
for (const c of Object.values(semanticCatalog)) {
|
||||||
assert.ok(c.execution);
|
assert.ok(c.execution);
|
||||||
@@ -114,6 +117,161 @@ test("catalog exhaustively mirrors all current contracts", () => {
|
|||||||
assert.ok(c.outputs);
|
assert.ok(c.outputs);
|
||||||
assert.ok(c.parameters);
|
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", () => {
|
test("adapter deterministically emits the canonical schema, permits repeated types, omits muted links and maps sources", () => {
|
||||||
const x = fixture(),
|
const x = fixture(),
|
||||||
@@ -123,16 +281,14 @@ test("adapter deterministically emits the canonical schema, permits repeated typ
|
|||||||
assert.deepEqual(adaptFxNodeSnapshot(x, 7), a);
|
assert.deepEqual(adaptFxNodeSnapshot(x, 7), a);
|
||||||
assert.equal(a.schemaVersion, 2);
|
assert.equal(a.schemaVersion, 2);
|
||||||
assert.equal(a.graphId, GRAPH_ID);
|
assert.equal(a.graphId, GRAPH_ID);
|
||||||
assert.equal(
|
assert.equal(a.nodes.filter((n) => n.executor.key === "texture").length, 2);
|
||||||
a.nodes.filter((n) => n.executor.key === "texture_spec").length,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
assert.ok(
|
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(
|
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,
|
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.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[0].id = "bad id"), "AUTHORING_ID");
|
||||||
reject((x) => (x.nodes[1].id = x.nodes[0].id), "AUTHORING_ID_DUPLICATE");
|
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].typeId = "wat"), "AUTHORING_NODE_TYPE");
|
||||||
reject((x) => (x.nodes[0].sockets = []), "AUTHORING_SOCKET_SET");
|
reject((x) => (x.nodes[0].sockets = []), "AUTHORING_SOCKET_SET");
|
||||||
reject((x) => (x.links[0].toSocketId = "missing:x"), "AUTHORING_LINK");
|
reject((x) => (x.links[0].toSocketId = "missing:x"), "AUTHORING_LINK");
|
||||||
reject((x) => {
|
reject((x) => {
|
||||||
const link = x.links.find((l) => l.toSocketId === "copy:source");
|
const link = x.links.find((l) => l.toSocketId === "frame_out:color");
|
||||||
link.fromNodeId = "scene";
|
link.fromNodeId = "mesh";
|
||||||
link.fromSocketId = "scene:scene";
|
link.fromSocketId = "mesh:mesh";
|
||||||
}, "AUTHORING_LINK_TYPE");
|
}, "AUTHORING_LINK_TYPE");
|
||||||
});
|
});
|
||||||
test("adapter counts only active incoming links and reports socket overflow", () => {
|
test("adapter counts only active incoming links and reports socket overflow", () => {
|
||||||
const x = fixture();
|
const x = fixture();
|
||||||
const active = x.links.find((link) => link.toSocketId === "copy:source");
|
const active = x.links.find((link) => link.toSocketId === "frame_out:color");
|
||||||
x.links.push({ ...structuredClone(active), id: "muted_duplicate", muted: true });
|
x.links.push({
|
||||||
|
...structuredClone(active),
|
||||||
|
id: "muted_duplicate",
|
||||||
|
muted: true,
|
||||||
|
});
|
||||||
assert.doesNotThrow(() => adaptFxNodeSnapshot(x));
|
assert.doesNotThrow(() => adaptFxNodeSnapshot(x));
|
||||||
x.links.push({ ...structuredClone(active), id: "active_overflow" });
|
x.links.push({ ...structuredClone(active), id: "active_overflow" });
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => adaptFxNodeSnapshot(x),
|
() => 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", () => {
|
test("source map covers Rust fields, nested values, every input and is deeply frozen", () => {
|
||||||
const snapshot = fixture();
|
const snapshot = fixture();
|
||||||
snapshot.links.find((link) => link.toSocketId === "copy:source").muted = true;
|
snapshot.links.find((link) => link.toSocketId === "frame_out:color").muted =
|
||||||
const ir = adaptFxNodeSnapshot(snapshot, 9), map = getSourceMap(ir);
|
true;
|
||||||
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"])
|
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);
|
assert.ok(map[path], path);
|
||||||
for (const [index, node] of ir.nodes.entries())
|
for (const [index, node] of ir.nodes.entries())
|
||||||
for (const input of Object.keys(semanticCatalog[node.executor.key].inputs))
|
for (const input of Object.keys(semanticCatalog[node.executor.key].inputs))
|
||||||
assert.ok(map[`nodes[${index}].inputs.${input}`]);
|
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 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");
|
const link = Object.values(map).find((source) => source.kind === "link");
|
||||||
assert.ok(socket?.socketId && unconnected?.socketId);
|
assert.ok(socket?.socketId && unconnected?.socketId);
|
||||||
assert.equal(ir.nodes.find((node) => node.id === "copy").inputs.source, undefined);
|
assert.equal(
|
||||||
for (const field of ["linkId", "fromNodeId", "fromSocketId", "toNodeId", "toSocketId", "muted"])
|
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.hasOwn(link, field), field);
|
||||||
assert.ok(Object.isFrozen(map) && Object.isFrozen(link));
|
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", () => {
|
test("diagnostic mapper creates a frozen RendererError DTO with fallbacks and prefix matching", () => {
|
||||||
const ir = adaptFxNodeSnapshot(fixture());
|
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);
|
const mapped = mapAuthoringDiagnostic(ir, original);
|
||||||
assert.notStrictEqual(mapped, original);
|
assert.notStrictEqual(mapped, original);
|
||||||
assert.equal(mapped.code, original.code);
|
assert.equal(mapped.code, original.code);
|
||||||
@@ -262,24 +533,44 @@ test("controller shares in-flight apply", async () => {
|
|||||||
await a;
|
await a;
|
||||||
});
|
});
|
||||||
test("controller retains mapped diagnostic while apply rejects the original and subscriptions agree", async () => {
|
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 states = [];
|
||||||
const c = new AuthoringController({
|
const c = new AuthoringController({
|
||||||
adapt: (snapshot, revision) => adaptFxNodeSnapshot(snapshot, revision),
|
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.subscribe((state) => states.push(state));
|
||||||
c.markDirty(fixture());
|
c.markDirty(fixture());
|
||||||
await assert.rejects(c.apply(), (error) => error === original);
|
await assert.rejects(c.apply(), (error) => error === original);
|
||||||
assert.notStrictEqual(states.at(-1).error, original);
|
assert.notStrictEqual(states.at(-1).error, original);
|
||||||
let subscribed;
|
let subscribed;
|
||||||
c.subscribe((state) => { subscribed = state; })();
|
c.subscribe((state) => {
|
||||||
|
subscribed = state;
|
||||||
|
})();
|
||||||
assert.strictEqual(subscribed.error, states.at(-1).error);
|
assert.strictEqual(subscribed.error, states.at(-1).error);
|
||||||
await c.destroy();
|
await c.destroy();
|
||||||
});
|
});
|
||||||
test("apply after destroy does not compile and destroy returns one strict promise", async () => {
|
test("apply after destroy does not compile and destroy returns one strict promise", async () => {
|
||||||
let compiles = 0;
|
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({});
|
c.markDirty({});
|
||||||
const first = c.destroy();
|
const first = c.destroy();
|
||||||
assert.strictEqual(c.destroy(), first);
|
assert.strictEqual(c.destroy(), first);
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {
|
import * as presets from "../static/render-graph/presets.js";
|
||||||
culling,
|
|
||||||
ember,
|
const order = [
|
||||||
hdr,
|
|
||||||
midnight,
|
|
||||||
renderGraphPresets,
|
|
||||||
} from "../static/render-graph/presets.js";
|
|
||||||
test("presets use canonical node graphs", () => {
|
|
||||||
assert.deepEqual(Object.keys(renderGraphPresets), [
|
|
||||||
"midnight",
|
"midnight",
|
||||||
"ember",
|
"ember",
|
||||||
"hdr",
|
"hdr",
|
||||||
@@ -17,75 +11,285 @@ test("presets use canonical node graphs", () => {
|
|||||||
"edges",
|
"edges",
|
||||||
"bloom",
|
"bloom",
|
||||||
"combined",
|
"combined",
|
||||||
]);
|
];
|
||||||
assert.deepEqual(
|
const sequences = {
|
||||||
[midnight.graphId, ember.graphId],
|
midnight: [
|
||||||
["preset_midnight", "preset_ember"],
|
["ldr", "texture"],
|
||||||
);
|
["depth", "texture"],
|
||||||
assert.notDeepEqual(midnight.nodes[6].parameters.clearColor, ember.nodes[6].parameters.clearColor);
|
["mesh", "mesh"],
|
||||||
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]),
|
|
||||||
[
|
|
||||||
["surface", "surface_target"],
|
|
||||||
["hdr", "texture_spec"],
|
|
||||||
["depth", "texture_spec"],
|
|
||||||
["scene", "scene_table"],
|
|
||||||
["visible", "visibility_flags"],
|
|
||||||
["query", "mesh_query"],
|
["query", "mesh_query"],
|
||||||
["depth_config", "depth_stencil_config"],
|
["registry", "pipeline_registry"],
|
||||||
["forward", "legacy_forward"],
|
["ground", "pipeline"],
|
||||||
["copy", "fullscreen_copy"],
|
["pbr", "pipeline"],
|
||||||
["present", "present"],
|
["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(
|
||||||
|
order.map((name) => presets[name].graphId),
|
||||||
|
[
|
||||||
|
"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]));
|
for (const name of order) {
|
||||||
assert.equal(byId.hdr.parameters.texture.format, "rgba16_float");
|
const graph = presets[name];
|
||||||
assert.deepEqual(byId.query.inputs, {
|
assert.deepEqual([graph.schemaVersion, graph.revision], [2, 1]);
|
||||||
scene: { node: "scene", socket: "scene" },
|
assert.equal(
|
||||||
isVisible: { node: "visible", socket: "flags" },
|
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.parameters.filters, [
|
assert.deepEqual(byId.query.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||||
{ flag: "isVisible", predicate: "required_true" },
|
assert.deepEqual(byId.query.inputs.isVisible, {
|
||||||
{ flag: "isFrustumCulled", predicate: "any" },
|
node: "mesh",
|
||||||
]);
|
socket: "isVisible",
|
||||||
assert.deepEqual(byId.forward.inputs.colorTarget, {
|
|
||||||
node: "hdr",
|
|
||||||
socket: "spec",
|
|
||||||
});
|
});
|
||||||
assert.deepEqual(byId.copy.executor, { key: "fullscreen_copy", version: 1 });
|
assert.deepEqual(byId.ground.inputs.mesh, {
|
||||||
assert.deepEqual(byId.copy.inputs, {
|
node: "mesh",
|
||||||
source: { node: "forward", socket: "color" },
|
socket: "mesh",
|
||||||
colorTarget: { node: "surface", socket: "surface" },
|
|
||||||
});
|
});
|
||||||
assert.deepEqual(byId.present.inputs.surface, {
|
assert.deepEqual(byId.ground.inputs.draws, {
|
||||||
node: "copy",
|
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",
|
socket: "color",
|
||||||
});
|
});
|
||||||
assert.deepEqual(
|
assert.deepEqual(byId.pbr.inputs.depthTarget, {
|
||||||
culling.nodes
|
node: "ground",
|
||||||
.filter((node) => ["frustum_cull", "mesh_query"].includes(node.executor.key))
|
socket: "depth",
|
||||||
.map((node) => node.executor.key),
|
|
||||||
["frustum_cull", "mesh_query"],
|
|
||||||
);
|
|
||||||
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",
|
|
||||||
});
|
});
|
||||||
|
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(cull.query.inputs.isFrustumCulled, {
|
||||||
|
node: "cull",
|
||||||
|
socket: "isFrustumCulled",
|
||||||
|
});
|
||||||
|
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",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user