feat: replace mesh queries with typed predicates
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:
@@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*;
|
||||
use renderer::app_setup::WebApp;
|
||||
use renderer::camera::Camera;
|
||||
use renderer::message::WindowEvent;
|
||||
use renderer::render_data::{MeshCreateInfo, RenderData, RenderFlags};
|
||||
use renderer::render_data::{InstanceType, MeshCreateInfo, RenderData};
|
||||
use renderer::renderer as gpu_renderer;
|
||||
use renderer::renderer::gpu_scene::vertex_layouts;
|
||||
use renderer::renderer::scene::FrameMetadata;
|
||||
@@ -193,8 +193,9 @@ impl EditorScene {
|
||||
indices: Self::INDICES,
|
||||
pipeline: pipeline_index,
|
||||
material: renderer::render_data::MaterialKey::DEFAULT,
|
||||
flags: RenderFlags::VISIBLE,
|
||||
default_instance_flags: RenderFlags::VISIBLE,
|
||||
default_instance_type: InstanceType {
|
||||
words: [1 | 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
default_transform: transform,
|
||||
})
|
||||
.expect("ground plane geometry is valid");
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
pub const MAGIC: u32 = u32::from_le_bytes(*b"YAWN");
|
||||
pub const VERSION: u32 = 1;
|
||||
pub const VERSION: u32 = 2;
|
||||
pub const CAPACITY: usize = 1024;
|
||||
pub const SLOT_WORDS: usize = 24;
|
||||
pub const SLOT_BYTES: usize = 96;
|
||||
pub const SLOT_WORDS: usize = 40;
|
||||
pub const SLOT_BYTES: usize = 160;
|
||||
pub const HEADER_BYTES: usize = 64;
|
||||
pub const SLOT_VERSION: u32 = 1;
|
||||
pub const SLOT_VERSION: u32 = 2;
|
||||
const STATE_OPEN: u32 = 0;
|
||||
const STATE_CORRUPT: u32 = 1;
|
||||
|
||||
@@ -121,7 +121,7 @@ mod tests {
|
||||
#[test]
|
||||
fn malformed_slot_fails_closed() {
|
||||
for (version, request, expected) in [
|
||||
(2, 1, RingError::SlotVersion),
|
||||
(SLOT_VERSION + 1, 1, RingError::SlotVersion),
|
||||
(SLOT_VERSION, 0, RingError::ZeroRequest),
|
||||
] {
|
||||
let ring = CommandRing::new();
|
||||
|
||||
+25
-6
@@ -4,8 +4,8 @@ use gltf::Gltf;
|
||||
use ultraviolet::{Mat4, Vec3};
|
||||
|
||||
use crate::render_data::{
|
||||
InstanceHandle, MaterialKey, MeshCreateInfo, MeshHandle, ModelTransform, PipelineKey,
|
||||
RenderData, RenderDataError, RenderFlags,
|
||||
InstanceHandle, InstanceType, MaterialKey, MeshCreateInfo, MeshHandle, ModelTransform,
|
||||
PipelineKey, RenderData, RenderDataError,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -549,8 +549,26 @@ pub fn install_imported(
|
||||
indices: &geometry.indices,
|
||||
pipeline: pipelines[usize::from(geometry.double_sided)],
|
||||
material: geometry.material,
|
||||
flags: RenderFlags::VISIBLE,
|
||||
default_instance_flags: RenderFlags::VISIBLE,
|
||||
default_instance_type: InstanceType {
|
||||
words: [
|
||||
1 | 4 | (geometry.double_sided as u32) * 8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
},
|
||||
default_transform: transform,
|
||||
})?;
|
||||
handles.insert(geometry.key, created.mesh);
|
||||
@@ -570,10 +588,11 @@ pub fn install_imported(
|
||||
.get(&occurrence.key)
|
||||
.ok_or_else(|| ImportError::InvalidPrimitive("occurrence has no geometry".into()))?;
|
||||
if consumed.insert(occurrence.key, ()).is_some() {
|
||||
let instance_type = stage.mesh(mesh).unwrap().default_instance_type;
|
||||
instance_handles.push(stage.create_instance(
|
||||
mesh,
|
||||
occurrence.transform,
|
||||
RenderFlags::VISIBLE,
|
||||
instance_type,
|
||||
)?);
|
||||
}
|
||||
let geometry = geometries
|
||||
@@ -584,7 +603,7 @@ pub fn install_imported(
|
||||
let point = transform.transform_point3(Vec3::from(*position));
|
||||
[point.x, point.y, point.z]
|
||||
}));
|
||||
let local = stage.mesh(mesh).unwrap().aabb;
|
||||
let local = stage.mesh(mesh).unwrap().local_aabb;
|
||||
for x in [local.min[0], local.max[0]] {
|
||||
for y in [local.min[1], local.max[1]] {
|
||||
for z in [local.min[2], local.max[2]] {
|
||||
|
||||
@@ -4,7 +4,7 @@ mod range_allocator;
|
||||
pub use handle::{InstanceHandle, MeshHandle};
|
||||
|
||||
use std::{
|
||||
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
|
||||
ops::Deref,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
@@ -58,50 +58,36 @@ impl MaterialKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RenderFlags(u32);
|
||||
|
||||
impl RenderFlags {
|
||||
pub const NONE: Self = Self(0);
|
||||
pub const VISIBLE: Self = Self(1);
|
||||
|
||||
pub const fn from_bits_retain(bits: u32) -> Self {
|
||||
Self(bits)
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct InstanceType {
|
||||
pub words: [u32; 16],
|
||||
}
|
||||
|
||||
pub const fn bits(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
impl InstanceType {
|
||||
pub const VISIBLE_MASK: u32 = 1;
|
||||
pub const ZERO: Self = Self { words: [0; 16] };
|
||||
pub const VISIBLE: Self = Self {
|
||||
words: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
|
||||
pub const fn contains(self, other: Self) -> bool {
|
||||
self.0 & other.0 == other.0
|
||||
pub const fn is_visible(self) -> bool {
|
||||
self.words[0] & Self::VISIBLE_MASK != 0
|
||||
}
|
||||
pub fn set_visible(&mut self, visible: bool) {
|
||||
self.words[0] = (self.words[0] & !Self::VISIBLE_MASK) | visible as u32;
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for RenderFlags {
|
||||
type Output = Self;
|
||||
fn bitor(self, rhs: Self) -> Self {
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
impl BitOrAssign for RenderFlags {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
self.0 |= rhs.0;
|
||||
}
|
||||
}
|
||||
impl BitAnd for RenderFlags {
|
||||
type Output = Self;
|
||||
fn bitand(self, rhs: Self) -> Self {
|
||||
Self(self.0 & rhs.0)
|
||||
}
|
||||
}
|
||||
impl BitAndAssign for RenderFlags {
|
||||
fn bitand_assign(&mut self, rhs: Self) {
|
||||
self.0 &= rhs.0;
|
||||
impl Default for InstanceType {
|
||||
fn default() -> Self {
|
||||
Self::VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
const _: [(); 64] = [(); std::mem::size_of::<InstanceType>()];
|
||||
const _: [(); 4] = [(); std::mem::align_of::<InstanceType>()];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Aabb {
|
||||
pub min: [f32; 3],
|
||||
@@ -124,8 +110,7 @@ pub struct MeshCreateInfo<'a> {
|
||||
pub indices: &'a [u32],
|
||||
pub pipeline: PipelineKey,
|
||||
pub material: MaterialKey,
|
||||
pub flags: RenderFlags,
|
||||
pub default_instance_flags: RenderFlags,
|
||||
pub default_instance_type: InstanceType,
|
||||
pub default_transform: ModelTransform,
|
||||
}
|
||||
|
||||
@@ -141,8 +126,8 @@ pub struct MeshView {
|
||||
pub geometry: GeometryRange,
|
||||
pub pipeline: PipelineKey,
|
||||
pub material: MaterialKey,
|
||||
pub flags: RenderFlags,
|
||||
pub aabb: Aabb,
|
||||
pub default_instance_type: InstanceType,
|
||||
pub local_aabb: Aabb,
|
||||
pub default_instance: InstanceHandle,
|
||||
}
|
||||
|
||||
@@ -152,7 +137,7 @@ pub struct InstanceView {
|
||||
pub mesh: MeshHandle,
|
||||
pub model: ModelTransform,
|
||||
pub normal: NormalMatrix,
|
||||
pub flags: RenderFlags,
|
||||
pub instance_type: InstanceType,
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
@@ -311,7 +296,7 @@ struct MeshSoa {
|
||||
index_counts: Vec<u32>,
|
||||
pipeline_keys: Vec<PipelineKey>,
|
||||
material_keys: Vec<MaterialKey>,
|
||||
flags: Vec<RenderFlags>,
|
||||
default_instance_types: Vec<InstanceType>,
|
||||
aabb_mins: Vec<[f32; 3]>,
|
||||
aabb_maxs: Vec<[f32; 3]>,
|
||||
default_instance_slots: Vec<u32>,
|
||||
@@ -329,7 +314,7 @@ struct InstanceSoa {
|
||||
normal_col_0: Vec<[f32; 3]>,
|
||||
normal_col_1: Vec<[f32; 3]>,
|
||||
normal_col_2: Vec<[f32; 3]>,
|
||||
flags: Vec<RenderFlags>,
|
||||
instance_types: Vec<InstanceType>,
|
||||
}
|
||||
|
||||
pub struct RenderData {
|
||||
@@ -369,9 +354,9 @@ impl ReplacementStage {
|
||||
&mut self,
|
||||
mesh: MeshHandle,
|
||||
model: ModelTransform,
|
||||
flags: RenderFlags,
|
||||
instance_type: InstanceType,
|
||||
) -> Result<InstanceHandle, RenderDataError> {
|
||||
self.data.create_instance(mesh, model, flags)
|
||||
self.data.create_instance(mesh, model, instance_type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +580,7 @@ impl RenderData {
|
||||
},
|
||||
info.pipeline,
|
||||
info.material,
|
||||
info.flags,
|
||||
info.default_instance_type,
|
||||
bounds,
|
||||
default_instance,
|
||||
);
|
||||
@@ -604,7 +589,7 @@ impl RenderData {
|
||||
mesh,
|
||||
info.default_transform,
|
||||
normal,
|
||||
info.default_instance_flags,
|
||||
info.default_instance_type,
|
||||
);
|
||||
self.revision = next_revision;
|
||||
Ok(CreatedMesh {
|
||||
@@ -617,19 +602,20 @@ impl RenderData {
|
||||
&mut self,
|
||||
mesh: MeshHandle,
|
||||
model: ModelTransform,
|
||||
flags: RenderFlags,
|
||||
instance_type: InstanceType,
|
||||
) -> Result<InstanceHandle, RenderDataError> {
|
||||
if !self.meshes.slots.contains(mesh.slot(), mesh.generation()) {
|
||||
return Err(RenderDataError::InvalidMeshHandle);
|
||||
}
|
||||
let normal = normal_matrix(model)?;
|
||||
affine_world_aabb(self.mesh(mesh).unwrap().aabb, model)?;
|
||||
affine_world_aabb(self.mesh(mesh).unwrap().local_aabb, model)?;
|
||||
let next_revision = self.next_revision()?;
|
||||
let required = self.instances.slots.required_len_for_prepare()?;
|
||||
self.instances.reserve(required)?;
|
||||
let prepared = self.instances.slots.prepare()?;
|
||||
let handle = InstanceHandle::from_parts(prepared.slot, prepared.generation);
|
||||
self.instances.commit(prepared, mesh, model, normal, flags);
|
||||
self.instances
|
||||
.commit(prepared, mesh, model, normal, instance_type);
|
||||
self.revision = next_revision;
|
||||
Ok(handle)
|
||||
}
|
||||
@@ -699,10 +685,10 @@ impl RenderData {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_mesh_flags(
|
||||
pub fn set_mesh_visible(
|
||||
&mut self,
|
||||
handle: MeshHandle,
|
||||
flags: RenderFlags,
|
||||
visible: bool,
|
||||
) -> Result<(), RenderDataError> {
|
||||
if !self
|
||||
.meshes
|
||||
@@ -711,16 +697,24 @@ impl RenderData {
|
||||
{
|
||||
return Err(RenderDataError::InvalidMeshHandle);
|
||||
}
|
||||
let owned: Vec<u32> = self
|
||||
.instances
|
||||
.slots
|
||||
.occupied()
|
||||
.filter_map(|(slot, _)| (self.instances.mesh_handle(slot) == handle).then_some(slot))
|
||||
.collect();
|
||||
let next_revision = self.next_revision()?;
|
||||
self.meshes.flags[handle.slot() as usize] = flags;
|
||||
for slot in owned {
|
||||
self.instances.instance_types[slot as usize].set_visible(visible);
|
||||
}
|
||||
self.revision = next_revision;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_instance_flags(
|
||||
pub fn set_instance_type(
|
||||
&mut self,
|
||||
handle: InstanceHandle,
|
||||
flags: RenderFlags,
|
||||
instance_type: InstanceType,
|
||||
) -> Result<(), RenderDataError> {
|
||||
if !self
|
||||
.instances
|
||||
@@ -730,7 +724,25 @@ impl RenderData {
|
||||
return Err(RenderDataError::InvalidInstanceHandle);
|
||||
}
|
||||
let next_revision = self.next_revision()?;
|
||||
self.instances.flags[handle.slot() as usize] = flags;
|
||||
self.instances.instance_types[handle.slot() as usize] = instance_type;
|
||||
self.revision = next_revision;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_instance_visible(
|
||||
&mut self,
|
||||
handle: InstanceHandle,
|
||||
visible: bool,
|
||||
) -> Result<(), RenderDataError> {
|
||||
if !self
|
||||
.instances
|
||||
.slots
|
||||
.contains(handle.slot(), handle.generation())
|
||||
{
|
||||
return Err(RenderDataError::InvalidInstanceHandle);
|
||||
}
|
||||
let next_revision = self.next_revision()?;
|
||||
self.instances.instance_types[handle.slot() as usize].set_visible(visible);
|
||||
self.revision = next_revision;
|
||||
Ok(())
|
||||
}
|
||||
@@ -749,7 +761,7 @@ impl RenderData {
|
||||
}
|
||||
let normal = normal_matrix(model)?;
|
||||
let mesh = self.instances.mesh_handle(handle.slot());
|
||||
affine_world_aabb(self.mesh(mesh).unwrap().aabb, model)?;
|
||||
affine_world_aabb(self.mesh(mesh).unwrap().local_aabb, model)?;
|
||||
let next_revision = self.next_revision()?;
|
||||
self.instances.set_transform(handle.slot(), model, normal);
|
||||
self.revision = next_revision;
|
||||
@@ -890,7 +902,7 @@ impl MeshSoa {
|
||||
index_counts: Vec::new(),
|
||||
pipeline_keys: Vec::new(),
|
||||
material_keys: Vec::new(),
|
||||
flags: Vec::new(),
|
||||
default_instance_types: Vec::new(),
|
||||
aabb_mins: Vec::new(),
|
||||
aabb_maxs: Vec::new(),
|
||||
default_instance_slots: Vec::new(),
|
||||
@@ -910,7 +922,7 @@ impl MeshSoa {
|
||||
reserve_vec(&mut self.index_counts, target, "meshes")?;
|
||||
reserve_vec(&mut self.pipeline_keys, target, "meshes")?;
|
||||
reserve_vec(&mut self.material_keys, target, "meshes")?;
|
||||
reserve_vec(&mut self.flags, target, "meshes")?;
|
||||
reserve_vec(&mut self.default_instance_types, target, "meshes")?;
|
||||
reserve_vec(&mut self.aabb_mins, target, "meshes")?;
|
||||
reserve_vec(&mut self.aabb_maxs, target, "meshes")?;
|
||||
reserve_vec(&mut self.default_instance_slots, target, "meshes")?;
|
||||
@@ -925,7 +937,7 @@ impl MeshSoa {
|
||||
geometry: GeometryRange,
|
||||
pipeline: PipelineKey,
|
||||
material: MaterialKey,
|
||||
flags: RenderFlags,
|
||||
default_instance_type: InstanceType,
|
||||
bounds: Aabb,
|
||||
default: InstanceHandle,
|
||||
) {
|
||||
@@ -936,7 +948,7 @@ impl MeshSoa {
|
||||
resize_column(&mut self.index_counts, len, 0);
|
||||
resize_column(&mut self.pipeline_keys, len, PipelineKey::new(0));
|
||||
resize_column(&mut self.material_keys, len, MaterialKey::DEFAULT);
|
||||
resize_column(&mut self.flags, len, RenderFlags::NONE);
|
||||
resize_column(&mut self.default_instance_types, len, InstanceType::ZERO);
|
||||
resize_column(&mut self.aabb_mins, len, [0.0; 3]);
|
||||
resize_column(&mut self.aabb_maxs, len, [0.0; 3]);
|
||||
resize_column(&mut self.default_instance_slots, len, 0);
|
||||
@@ -948,7 +960,7 @@ impl MeshSoa {
|
||||
self.index_counts[index] = geometry.index_count;
|
||||
self.pipeline_keys[index] = pipeline;
|
||||
self.material_keys[index] = material;
|
||||
self.flags[index] = flags;
|
||||
self.default_instance_types[index] = default_instance_type;
|
||||
self.aabb_mins[index] = bounds.min;
|
||||
self.aabb_maxs[index] = bounds.max;
|
||||
self.default_instance_slots[index] = default.slot();
|
||||
@@ -968,8 +980,8 @@ impl MeshSoa {
|
||||
},
|
||||
pipeline: self.pipeline_keys[index],
|
||||
material: self.material_keys[index],
|
||||
flags: self.flags[index],
|
||||
aabb: Aabb {
|
||||
default_instance_type: self.default_instance_types[index],
|
||||
local_aabb: Aabb {
|
||||
min: self.aabb_mins[index],
|
||||
max: self.aabb_maxs[index],
|
||||
},
|
||||
@@ -994,7 +1006,7 @@ impl InstanceSoa {
|
||||
normal_col_0: Vec::new(),
|
||||
normal_col_1: Vec::new(),
|
||||
normal_col_2: Vec::new(),
|
||||
flags: Vec::new(),
|
||||
instance_types: Vec::new(),
|
||||
};
|
||||
soa.reserve(initial)?;
|
||||
Ok(soa)
|
||||
@@ -1013,7 +1025,7 @@ impl InstanceSoa {
|
||||
reserve_vec(&mut self.normal_col_0, target, "instances")?;
|
||||
reserve_vec(&mut self.normal_col_1, target, "instances")?;
|
||||
reserve_vec(&mut self.normal_col_2, target, "instances")?;
|
||||
reserve_vec(&mut self.flags, target, "instances")?;
|
||||
reserve_vec(&mut self.instance_types, target, "instances")?;
|
||||
self.slots.reserve_for_len(target, "instances")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1024,7 +1036,7 @@ impl InstanceSoa {
|
||||
mesh: MeshHandle,
|
||||
model: ModelTransform,
|
||||
normal: NormalMatrix,
|
||||
flags: RenderFlags,
|
||||
instance_type: InstanceType,
|
||||
) {
|
||||
let len = prepared.slot as usize + 1;
|
||||
resize_column(&mut self.mesh_slots, len, 0);
|
||||
@@ -1036,11 +1048,11 @@ impl InstanceSoa {
|
||||
resize_column(&mut self.normal_col_0, len, [0.0; 3]);
|
||||
resize_column(&mut self.normal_col_1, len, [0.0; 3]);
|
||||
resize_column(&mut self.normal_col_2, len, [0.0; 3]);
|
||||
resize_column(&mut self.flags, len, RenderFlags::NONE);
|
||||
resize_column(&mut self.instance_types, len, InstanceType::ZERO);
|
||||
let index = prepared.slot as usize;
|
||||
self.mesh_slots[index] = mesh.slot();
|
||||
self.mesh_generations[index] = mesh.generation();
|
||||
self.flags[index] = flags;
|
||||
self.instance_types[index] = instance_type;
|
||||
self.set_transform(prepared.slot, model, normal);
|
||||
self.slots.commit(prepared);
|
||||
}
|
||||
@@ -1077,7 +1089,7 @@ impl InstanceSoa {
|
||||
self.normal_col_1[index],
|
||||
self.normal_col_2[index],
|
||||
],
|
||||
flags: self.flags[index],
|
||||
instance_type: self.instance_types[index],
|
||||
is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ fn info() -> MeshCreateInfo<'static> {
|
||||
indices: &INDICES,
|
||||
pipeline: PipelineKey::new(7),
|
||||
material: MaterialKey::new(11),
|
||||
flags: RenderFlags::from_bits_retain(2),
|
||||
default_instance_flags: RenderFlags::VISIBLE,
|
||||
default_instance_type: InstanceType {
|
||||
words: [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
default_transform: IDENTITY_MODEL_TRANSFORM,
|
||||
}
|
||||
}
|
||||
@@ -81,29 +82,42 @@ fn world_bounds_reject_projective_and_overflowing_transforms() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_instance_is_protected_and_flags_are_separate() {
|
||||
fn default_instance_is_protected_and_preserves_its_type() {
|
||||
let mut data = data();
|
||||
let created = data.create_mesh(info()).unwrap();
|
||||
assert!(data.instance(created.default_instance).unwrap().is_default);
|
||||
assert_eq!(data.mesh(created.mesh).unwrap().flags.bits(), 2);
|
||||
assert_eq!(
|
||||
data.mesh(created.mesh).unwrap().default_instance_type.words[0],
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
data.mesh(created.mesh).unwrap().material,
|
||||
MaterialKey::new(11)
|
||||
);
|
||||
assert_eq!(
|
||||
data.instance(created.default_instance).unwrap().flags,
|
||||
RenderFlags::VISIBLE
|
||||
data.instance(created.default_instance)
|
||||
.unwrap()
|
||||
.instance_type,
|
||||
info().default_instance_type
|
||||
);
|
||||
assert_eq!(
|
||||
data.destroy_instance(created.default_instance),
|
||||
Err(RenderDataError::CannotDestroyDefaultInstance)
|
||||
);
|
||||
data.set_mesh_flags(created.mesh, RenderFlags::NONE)
|
||||
.unwrap();
|
||||
assert_eq!(data.mesh(created.mesh).unwrap().flags, RenderFlags::NONE);
|
||||
data.set_mesh_visible(created.mesh, false).unwrap();
|
||||
assert_eq!(
|
||||
data.instance(created.default_instance).unwrap().flags,
|
||||
RenderFlags::VISIBLE
|
||||
data.mesh(created.mesh).unwrap().default_instance_type,
|
||||
info().default_instance_type
|
||||
);
|
||||
assert_eq!(
|
||||
data.instance(created.default_instance)
|
||||
.unwrap()
|
||||
.instance_type,
|
||||
{
|
||||
let mut expected = info().default_instance_type;
|
||||
expected.set_visible(false);
|
||||
expected
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,11 +126,11 @@ fn stale_mesh_and_instance_handles_are_rejected_after_reuse() {
|
||||
let mut data = data();
|
||||
let first = data.create_mesh(info()).unwrap();
|
||||
let old_instance = data
|
||||
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
data.destroy_instance(old_instance).unwrap();
|
||||
let replacement = data
|
||||
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
assert_eq!(old_instance.slot(), replacement.slot());
|
||||
assert_ne!(old_instance.generation(), replacement.generation());
|
||||
@@ -133,7 +147,7 @@ fn clear_handles_all_slot_states_retains_capacity_and_never_reuses_retired() {
|
||||
let mut data = data();
|
||||
let mesh = data.create_mesh(info()).unwrap();
|
||||
let vacant = data
|
||||
.create_instance(mesh.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
.create_instance(mesh.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
data.destroy_instance(vacant).unwrap();
|
||||
data.instances
|
||||
@@ -188,7 +202,7 @@ fn all_storage_classes_grow_and_retired_slots_force_max_checked_append() {
|
||||
instances: 1,
|
||||
}
|
||||
);
|
||||
data.create_instance(mesh.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
data.create_instance(mesh.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
assert_eq!(data.capacities().instances, 2);
|
||||
|
||||
@@ -302,7 +316,7 @@ fn aabb_supports_one_point_and_multiple_points() {
|
||||
let mut data = data();
|
||||
let mesh = data.create_mesh(one).unwrap();
|
||||
assert_eq!(
|
||||
data.mesh(mesh.mesh).unwrap().aabb,
|
||||
data.mesh(mesh.mesh).unwrap().local_aabb,
|
||||
Aabb {
|
||||
min: point[0],
|
||||
max: point[0]
|
||||
@@ -310,7 +324,7 @@ fn aabb_supports_one_point_and_multiple_points() {
|
||||
);
|
||||
let mesh = data.create_mesh(info()).unwrap();
|
||||
assert_eq!(
|
||||
data.mesh(mesh.mesh).unwrap().aabb,
|
||||
data.mesh(mesh.mesh).unwrap().local_aabb,
|
||||
Aabb {
|
||||
min: [-1.0, -2.0, -3.0],
|
||||
max: [4.0, 2.0, 3.0],
|
||||
@@ -444,7 +458,7 @@ fn normal_matrices_and_failed_transform_operations_are_transactional() {
|
||||
assert_eq!(data.instance(mesh.default_instance).unwrap(), old);
|
||||
let count = data.instance_count();
|
||||
assert_eq!(
|
||||
data.create_instance(mesh.mesh, invalid, RenderFlags::NONE),
|
||||
data.create_instance(mesh.mesh, invalid, InstanceType::ZERO),
|
||||
Err(RenderDataError::InvalidTransform)
|
||||
);
|
||||
assert_eq!(data.instance_count(), count);
|
||||
@@ -463,14 +477,14 @@ fn destroying_mesh_invalidates_exact_owner_instances_with_reused_generations() {
|
||||
let first = data.create_mesh(info()).unwrap();
|
||||
let second = data.create_mesh(info()).unwrap();
|
||||
let first_extra = data
|
||||
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
let second_extra = data
|
||||
.create_instance(second.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
.create_instance(second.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
data.destroy_instance(first_extra).unwrap();
|
||||
let reused = data
|
||||
.create_instance(second.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
|
||||
.create_instance(second.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
assert_eq!(first_extra.slot(), reused.slot());
|
||||
data.destroy_mesh(first.mesh).unwrap();
|
||||
|
||||
@@ -21,20 +21,13 @@ struct CullParameters {
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
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],
|
||||
predicate_default: bool,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -146,7 +139,6 @@ struct OutputKey(usize, u16);
|
||||
#[derive(Clone, Copy)]
|
||||
struct BoundInput {
|
||||
producer: OutputKey,
|
||||
active: bool,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct DependencyEdge {
|
||||
@@ -232,15 +224,6 @@ fn validate_name_grammar(s: &str, path: impl Into<String>) -> Result<(), GraphEr
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mesh_predicate_matches(predicate: RuntimePredicate, flag: bool) -> bool {
|
||||
match predicate {
|
||||
RuntimePredicate::Any => true,
|
||||
RuntimePredicate::RequiredTrue => flag,
|
||||
RuntimePredicate::RequiredFalse => !flag,
|
||||
RuntimePredicate::Never => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_and_compile(bytes: &[u8]) -> Result<CompiledGraph, GraphError> {
|
||||
if bytes.len() > MAX_JSON_BYTES {
|
||||
return Err(GraphError::new(
|
||||
@@ -424,6 +407,89 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
$variant
|
||||
}};
|
||||
}
|
||||
fn literal(value: &serde_json::Value, ty: SemanticType) -> Option<TypedLiteral> {
|
||||
let floats = |value: &serde_json::Value, n: usize| -> Option<Vec<f32>> {
|
||||
let values = value.as_array()?;
|
||||
if values.len() != n {
|
||||
return None;
|
||||
}
|
||||
values
|
||||
.iter()
|
||||
.map(|value| value.as_f64().map(|value| value as f32))
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.filter(|values| values.iter().all(|value| value.is_finite()))
|
||||
};
|
||||
let vector = |n| floats(value, n);
|
||||
Some(match ty {
|
||||
SemanticType::Bool => TypedLiteral::Bool(value.as_bool()?),
|
||||
SemanticType::F32 => {
|
||||
let value = value.as_f64()? as f32;
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
TypedLiteral::F32(value)
|
||||
}
|
||||
SemanticType::U32 => TypedLiteral::U32(value.as_u64()?.try_into().ok()?),
|
||||
SemanticType::Vec2 => TypedLiteral::Vec2(vector(2)?.try_into().ok()?),
|
||||
SemanticType::Vec3 => TypedLiteral::Vec3(vector(3)?.try_into().ok()?),
|
||||
SemanticType::Vec4 => TypedLiteral::Vec4(vector(4)?.try_into().ok()?),
|
||||
SemanticType::U32x16 => TypedLiteral::U32x16(
|
||||
value
|
||||
.as_array()?
|
||||
.iter()
|
||||
.map(|v| v.as_u64()?.try_into().ok())
|
||||
.collect::<Option<Vec<u32>>>()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
),
|
||||
SemanticType::LocalAabb => TypedLiteral::LocalAabb {
|
||||
min: floats(value.get("min")?, 3)?.try_into().ok()?,
|
||||
max: floats(value.get("max")?, 3)?.try_into().ok()?,
|
||||
},
|
||||
ty @ (SemanticType::Mat2 | SemanticType::Mat3 | SemanticType::Mat4) => {
|
||||
let n = match ty {
|
||||
SemanticType::Mat2 => 2,
|
||||
SemanticType::Mat3 => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let columns = value.as_array()?;
|
||||
if columns.len() != n {
|
||||
return None;
|
||||
}
|
||||
let columns = columns
|
||||
.iter()
|
||||
.map(|v| floats(v, n))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
match ty {
|
||||
SemanticType::Mat2 => TypedLiteral::Mat2(
|
||||
columns
|
||||
.into_iter()
|
||||
.map(|v| v.try_into().ok())
|
||||
.collect::<Option<Vec<_>>>()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
),
|
||||
SemanticType::Mat3 => TypedLiteral::Mat3(
|
||||
columns
|
||||
.into_iter()
|
||||
.map(|v| v.try_into().ok())
|
||||
.collect::<Option<Vec<_>>>()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
),
|
||||
_ => TypedLiteral::Mat4(
|
||||
columns
|
||||
.into_iter()
|
||||
.map(|v| v.try_into().ok())
|
||||
.collect::<Option<Vec<_>>>()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
),
|
||||
}
|
||||
}
|
||||
SemanticType::MeshData | SemanticType::Texture => return None,
|
||||
})
|
||||
}
|
||||
Ok(match node.executor.key.as_str() {
|
||||
"mesh" => empty!(NormalizedParameters::Mesh),
|
||||
"frustum_cull" => {
|
||||
@@ -607,37 +673,6 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
descriptor: normalize_texture(p.texture, &base)?,
|
||||
}
|
||||
}
|
||||
"mesh_query" => {
|
||||
let p: QueryParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
let fold = |predicate, default, linked| match (predicate, linked, default) {
|
||||
(TriStatePredicate::Any, _, _) => RuntimePredicate::Any,
|
||||
(TriStatePredicate::RequiredTrue, true, _) => RuntimePredicate::RequiredTrue,
|
||||
(TriStatePredicate::RequiredFalse, true, _) => RuntimePredicate::RequiredFalse,
|
||||
(TriStatePredicate::RequiredTrue, false, true)
|
||||
| (TriStatePredicate::RequiredFalse, false, false) => RuntimePredicate::Any,
|
||||
_ => RuntimePredicate::Never,
|
||||
};
|
||||
let mut visible = fold(
|
||||
p.visible_predicate,
|
||||
p.visible_default,
|
||||
node.inputs.contains_key("isVisible"),
|
||||
);
|
||||
let mut culled = fold(
|
||||
p.frustum_culled_predicate,
|
||||
p.frustum_culled_default,
|
||||
node.inputs.contains_key("isFrustumCulled"),
|
||||
);
|
||||
if visible == RuntimePredicate::Never || culled == RuntimePredicate::Never {
|
||||
visible = RuntimePredicate::Never;
|
||||
culled = RuntimePredicate::Never;
|
||||
}
|
||||
NormalizedParameters::MeshQuery {
|
||||
visible_predicate: visible,
|
||||
frustum_culled_predicate: culled,
|
||||
}
|
||||
}
|
||||
"pipeline_registry" => empty!(NormalizedParameters::PipelineRegistry),
|
||||
"pipeline" => {
|
||||
let p: PipelineParameters =
|
||||
serde_json::from_value(node.parameters.clone()).map_err(invalid)?;
|
||||
@@ -673,8 +708,50 @@ fn decode(node: &Node, i: usize) -> Result<NormalizedParameters, GraphError> {
|
||||
depth_write_enabled: p.depth_write_enabled,
|
||||
clear_depth: p.clear_depth,
|
||||
clear_color: p.clear_color,
|
||||
predicate_default: p.predicate_default,
|
||||
}
|
||||
}
|
||||
key if contract(key)
|
||||
.is_some_and(|contract| contract.execution == ExecutionClass::Expression) =>
|
||||
{
|
||||
let contract = contract(key).unwrap();
|
||||
let object = node.parameters.as_object().ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"parameters must be an object",
|
||||
base.clone(),
|
||||
)
|
||||
})?;
|
||||
if object.len() != contract.inputs.len() {
|
||||
return Err(error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"expression defaults must exactly match inputs",
|
||||
base,
|
||||
));
|
||||
}
|
||||
let mut defaults = Vec::with_capacity(contract.inputs.len());
|
||||
for input in contract.inputs {
|
||||
let key = format!("{}Default", input.name);
|
||||
let value = object.get(&key).ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"missing expression default",
|
||||
format!("{base}.{key}"),
|
||||
)
|
||||
})?;
|
||||
let TypeConstraint::Exact(ty) = input.accepted else {
|
||||
unreachable!()
|
||||
};
|
||||
defaults.push(literal(value, ty).ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_PARAMETERS_INVALID",
|
||||
"invalid typed expression default",
|
||||
format!("{base}.{key}"),
|
||||
)
|
||||
})?);
|
||||
}
|
||||
NormalizedParameters::ExpressionDefaults { defaults }
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
@@ -846,13 +923,8 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
for (i, n) in graph.nodes.iter().enumerate() {
|
||||
for input in contracts[i].inputs {
|
||||
let inactive = matches!(¶ms[i], NormalizedParameters::MeshQuery { visible_predicate, frustum_culled_predicate } if (input.name == "isVisible" && matches!(visible_predicate, RuntimePredicate::Any | RuntimePredicate::Never)) || (input.name == "isFrustumCulled" && matches!(frustum_culled_predicate, RuntimePredicate::Any | RuntimePredicate::Never)));
|
||||
if !n.inputs.contains_key(input.name) {
|
||||
if input.cardinality == InputCardinality::RequiredOne
|
||||
|| (!inactive
|
||||
&& matches!(params[i], NormalizedParameters::MeshQuery { .. })
|
||||
&& input.name != "mesh")
|
||||
{
|
||||
if input.cardinality == InputCardinality::RequiredOne {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_CARDINALITY",
|
||||
"required input is missing",
|
||||
@@ -866,7 +938,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let mut bound: Vec<BTreeMap<&str, BoundInput>> = vec![BTreeMap::new(); graph.nodes.len()];
|
||||
for (i, n) in graph.nodes.iter().enumerate() {
|
||||
for input in contracts[i].inputs {
|
||||
let inactive = matches!(¶ms[i], NormalizedParameters::MeshQuery { visible_predicate, frustum_culled_predicate } if (input.name == "isVisible" && matches!(visible_predicate, RuntimePredicate::Any | RuntimePredicate::Never)) || (input.name == "isFrustumCulled" && matches!(frustum_culled_predicate, RuntimePredicate::Any | RuntimePredicate::Never)));
|
||||
let Some(r) = n.inputs.get(input.name) else {
|
||||
continue;
|
||||
};
|
||||
@@ -884,97 +955,18 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
format!("nodes[{i}].inputs.{}", input.name),
|
||||
));
|
||||
}
|
||||
if let Some(flag) = MeshFlag::ORDERED
|
||||
.iter()
|
||||
.find(|f| f.input_socket() == input.name)
|
||||
{
|
||||
if out.metadata != (OutputMetadata::BooleanFlag { flag: *flag }) {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"mesh flag metadata mismatch",
|
||||
format!("nodes[{i}].inputs.{}", input.name),
|
||||
));
|
||||
}
|
||||
}
|
||||
bound[i].insert(
|
||||
input.name,
|
||||
BoundInput {
|
||||
producer: OutputKey(pn, ordinal as u16),
|
||||
active: !inactive,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
let root = |key: OutputKey,
|
||||
bound: &Vec<BTreeMap<&str, BoundInput>>,
|
||||
contracts: &Vec<&Contract>|
|
||||
-> Option<OutputKey> {
|
||||
let mut k = key;
|
||||
let mut seen = HashSet::new();
|
||||
loop {
|
||||
if !seen.insert(k.0) {
|
||||
return None;
|
||||
}
|
||||
if contracts[k.0].key == "mesh" {
|
||||
let ordinal = contracts[k.0]
|
||||
.outputs
|
||||
.iter()
|
||||
.position(|output| output.semantic_type == SemanticType::MeshData)?;
|
||||
return Some(OutputKey(k.0, ordinal as u16));
|
||||
}
|
||||
if contracts[k.0].outputs[k.1 as usize].semantic_type == SemanticType::MeshData {
|
||||
return Some(k);
|
||||
}
|
||||
k = bound[k.0]
|
||||
.get("mesh")
|
||||
.or_else(|| bound[k.0].get("pipelineIndices"))
|
||||
.or_else(|| bound[k.0].get("activation"))?
|
||||
.producer;
|
||||
}
|
||||
};
|
||||
for (i, c) in contracts.iter().enumerate() {
|
||||
if c.key == "frustum_cull"
|
||||
&& root(bound[i]["mesh"].producer, &bound, &contracts)
|
||||
!= root(bound[i]["localAabbs"].producer, &bound, &contracts)
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"scene roots differ",
|
||||
format!("nodes[{i}].inputs.localAabbs"),
|
||||
));
|
||||
}
|
||||
if matches!(c.key, "mesh_query" | "pipeline_registry" | "pipeline") {
|
||||
let scene_socket = if c.key == "pipeline_registry" {
|
||||
"pipelineIndices"
|
||||
} else {
|
||||
"mesh"
|
||||
};
|
||||
let scene = root(bound[i][scene_socket].producer, &bound, &contracts);
|
||||
for (s, b) in &bound[i] {
|
||||
if b.active
|
||||
&& matches!(
|
||||
*s,
|
||||
"isVisible"
|
||||
| "isFrustumCulled"
|
||||
| "draws"
|
||||
| "pipelineIndices"
|
||||
| "activation"
|
||||
)
|
||||
&& root(b.producer, &bound, &contracts) != scene
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"scene roots differ",
|
||||
format!("nodes[{i}].inputs.{s}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..graph.nodes.len() {
|
||||
for (input_ordinal, input) in contracts[i].inputs.iter().enumerate() {
|
||||
if let Some(b) = bound[i].get(input.name).filter(|b| b.active) {
|
||||
if let Some(b) = bound[i].get(input.name) {
|
||||
edges.push(DependencyEdge {
|
||||
from_node: b.producer.0,
|
||||
from_socket: contracts[b.producer.0].outputs[b.producer.1 as usize]
|
||||
@@ -1030,6 +1022,9 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
for i in 0..graph.nodes.len() {
|
||||
if live.contains(&i) {
|
||||
for (o, out) in contracts[i].outputs.iter().enumerate() {
|
||||
if out.semantic_type.is_virtual() {
|
||||
continue;
|
||||
}
|
||||
let key = OutputKey(i, o as u16);
|
||||
if contracts[i].execution == ExecutionClass::Source
|
||||
&& !referenced_outputs.contains(&key)
|
||||
@@ -1586,7 +1581,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
for (i, o, out) in resource_meta {
|
||||
let key = OutputKey(i, o);
|
||||
let id = output_ids[&key];
|
||||
let mesh = || output_ids[&root(key, &bound, &contracts).unwrap()];
|
||||
let plan = match out.semantic_type {
|
||||
SemanticType::Texture if matches!(params[i], NormalizedParameters::Texture { .. }) => {
|
||||
if let NormalizedParameters::Texture {
|
||||
@@ -1615,19 +1609,19 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
}
|
||||
}
|
||||
SemanticType::MeshData => ResourcePlan::MeshData,
|
||||
SemanticType::LocalAabbBuffer => ResourcePlan::LocalAabbBuffer { mesh: mesh() },
|
||||
SemanticType::BooleanFlagBuffer => {
|
||||
if let OutputMetadata::BooleanFlag { flag } = out.metadata {
|
||||
ResourcePlan::BooleanFlagBuffer { mesh: mesh(), flag }
|
||||
} else {
|
||||
unreachable!()
|
||||
SemanticType::Bool
|
||||
| SemanticType::F32
|
||||
| SemanticType::U32
|
||||
| SemanticType::Vec2
|
||||
| SemanticType::Vec3
|
||||
| SemanticType::Vec4
|
||||
| SemanticType::Mat2
|
||||
| SemanticType::Mat3
|
||||
| SemanticType::Mat4
|
||||
| SemanticType::U32x16
|
||||
| SemanticType::LocalAabb => {
|
||||
unreachable!("pure expression outputs are never materialized")
|
||||
}
|
||||
}
|
||||
SemanticType::PipelineIndexStream => ResourcePlan::PipelineIndexStream { mesh: mesh() },
|
||||
SemanticType::PipelineActivation => ResourcePlan::PipelineActivation {
|
||||
pipeline_indices: output_ids[&bound[i]["pipelineIndices"].producer],
|
||||
},
|
||||
SemanticType::DrawStream => ResourcePlan::DrawStream { mesh: mesh() },
|
||||
};
|
||||
resources.push(CompiledResource {
|
||||
original_node_index: i as u32,
|
||||
@@ -1644,17 +1638,20 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
let _ = id;
|
||||
}
|
||||
let mut executions = Vec::new();
|
||||
let mut node_execution = HashMap::new();
|
||||
for &i in &order {
|
||||
if contracts[i].execution == ExecutionClass::Source {
|
||||
if matches!(
|
||||
contracts[i].execution,
|
||||
ExecutionClass::Source | ExecutionClass::Expression
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let ordinal = executions.len() as u32;
|
||||
node_execution.insert(i, ordinal);
|
||||
let input_resource = |s: &str| output_ids[&bound[i][s].producer];
|
||||
let mut inputs = Vec::new();
|
||||
for s in contracts[i].inputs {
|
||||
if let Some(b) = bound[i].get(s.name).filter(|b| b.active) {
|
||||
if s.role != InputRole::Expression {
|
||||
let Some(b) = bound[i].get(s.name) else {
|
||||
continue;
|
||||
};
|
||||
inputs.push(CompiledSocketInput {
|
||||
socket: s.name.into(),
|
||||
resource: output_ids[&b.producer],
|
||||
@@ -1665,6 +1662,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
.outputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| !s.semantic_type.is_virtual())
|
||||
.map(|(o, s)| CompiledSocketOutput {
|
||||
socket: s.name.into(),
|
||||
resource: output_ids[&OutputKey(i, o as u16)],
|
||||
@@ -1672,58 +1670,6 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
.collect();
|
||||
let mut accesses = Vec::new();
|
||||
let kind = match contracts[i].key {
|
||||
"frustum_cull" => {
|
||||
for (s, m) in [
|
||||
("mesh", AccessMode::StorageRead),
|
||||
("localAabbs", AccessMode::StorageRead),
|
||||
] {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: s.into(),
|
||||
resource: input_resource(s),
|
||||
mode: m,
|
||||
});
|
||||
}
|
||||
let r = output_ids[&OutputKey(i, 0)];
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "isFrustumCulled".into(),
|
||||
resource: r,
|
||||
mode: AccessMode::StorageWrite {
|
||||
full_overwrite: true,
|
||||
},
|
||||
});
|
||||
ExecutionKind::Compute {
|
||||
work: ComputeWork::FrustumCull,
|
||||
}
|
||||
}
|
||||
"mesh_query" => {
|
||||
for s in ["mesh", "isVisible", "isFrustumCulled"] {
|
||||
if let Some(b) = bound[i].get(s).filter(|b| b.active) {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: s.into(),
|
||||
resource: output_ids[&b.producer],
|
||||
mode: AccessMode::StorageRead,
|
||||
});
|
||||
}
|
||||
}
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "draws".into(),
|
||||
resource: output_ids[&OutputKey(i, 0)],
|
||||
mode: AccessMode::StorageWrite {
|
||||
full_overwrite: true,
|
||||
},
|
||||
});
|
||||
ExecutionKind::Compute {
|
||||
work: ComputeWork::MeshQuery,
|
||||
}
|
||||
}
|
||||
"pipeline_registry" => {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: "pipelineIndices".into(),
|
||||
resource: input_resource("pipelineIndices"),
|
||||
mode: AccessMode::SemanticRead,
|
||||
});
|
||||
ExecutionKind::CpuPreparation
|
||||
}
|
||||
"pipeline" => {
|
||||
let color = output_ids[&OutputKey(i, 0)];
|
||||
let depth = output_ids[&OutputKey(i, 1)];
|
||||
@@ -1747,15 +1693,11 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
} else {
|
||||
NormalizedDepthLoad::Load
|
||||
};
|
||||
for s in ["mesh", "draws", "activation"] {
|
||||
for s in ["mesh"] {
|
||||
accesses.push(CompiledAccess {
|
||||
socket: s.into(),
|
||||
resource: input_resource(s),
|
||||
mode: if s == "draws" {
|
||||
AccessMode::IndirectRead
|
||||
} else {
|
||||
AccessMode::SemanticRead
|
||||
},
|
||||
mode: AccessMode::SemanticRead,
|
||||
});
|
||||
}
|
||||
accesses.push(CompiledAccess {
|
||||
@@ -1849,6 +1791,270 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
accesses,
|
||||
});
|
||||
}
|
||||
// Lower virtual values into a stable, device-independent expression plan.
|
||||
let mut expression_plan = ExpressionPlan::default();
|
||||
let mut expression_ids = HashMap::<OutputKey, ExprId>::new();
|
||||
let mut expression_provenance = HashMap::<OutputKey, Option<u32>>::new();
|
||||
let mut cse = HashMap::<String, ExprId>::new();
|
||||
let mut requires_camera = false;
|
||||
let mut mesh_root = None;
|
||||
let mut intern = |semantic_type: SemanticType,
|
||||
op: ExpressionOp,
|
||||
origin: NodeOutputRef,
|
||||
mesh_provenance: Option<u32>| {
|
||||
let key = format!("{semantic_type:?}:{op:?}:{mesh_provenance:?}");
|
||||
if let Some(id) = cse.get(&key) {
|
||||
return *id;
|
||||
}
|
||||
let id = ExprId(expression_plan.expressions.len() as u32);
|
||||
expression_plan.expressions.push(Expression {
|
||||
semantic_type,
|
||||
op,
|
||||
origin,
|
||||
mesh_provenance,
|
||||
});
|
||||
cse.insert(key, id);
|
||||
id
|
||||
};
|
||||
for &i in &order {
|
||||
if contracts[i].execution != ExecutionClass::Expression {
|
||||
continue;
|
||||
}
|
||||
let defaults: &[TypedLiteral] = match ¶ms[i] {
|
||||
NormalizedParameters::ExpressionDefaults { defaults } => defaults.as_slice(),
|
||||
NormalizedParameters::FrustumCull { .. } => &[],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut operands = Vec::new();
|
||||
let mut operand_provenance = Vec::new();
|
||||
for (ordinal, input) in contracts[i].inputs.iter().enumerate() {
|
||||
if let Some(binding) = bound[i].get(input.name) {
|
||||
let key = binding.producer;
|
||||
let producer_type = contracts[key.0].outputs[key.1 as usize].semantic_type;
|
||||
let operand_mesh = if contracts[key.0].key == "mesh" {
|
||||
Some(output_ids[&OutputKey(key.0, 0)])
|
||||
} else {
|
||||
expression_provenance[&key]
|
||||
};
|
||||
let id = if producer_type.is_virtual() {
|
||||
if contracts[key.0].key == "mesh" {
|
||||
let mesh = output_ids[&OutputKey(key.0, 0)];
|
||||
if mesh_root.is_some_and(|root| root != mesh) {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"instance traversal has multiple mesh roots",
|
||||
format!("nodes[{i}].inputs.{}", input.name),
|
||||
));
|
||||
}
|
||||
mesh_root = Some(mesh);
|
||||
let op = match producer_type {
|
||||
SemanticType::U32x16 => ExpressionOp::InstanceType { mesh },
|
||||
SemanticType::LocalAabb => ExpressionOp::LocalAabb { mesh },
|
||||
_ => unreachable!(),
|
||||
};
|
||||
intern(
|
||||
producer_type,
|
||||
op,
|
||||
graph.nodes[key.0]
|
||||
.inputs
|
||||
.get("")
|
||||
.cloned()
|
||||
.unwrap_or(NodeOutputRef {
|
||||
node: graph.nodes[key.0].id.clone(),
|
||||
socket: contracts[key.0].outputs[key.1 as usize].name.into(),
|
||||
}),
|
||||
Some(mesh),
|
||||
)
|
||||
} else {
|
||||
expression_ids[&key]
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
operands.push(id);
|
||||
operand_provenance.push(operand_mesh);
|
||||
} else {
|
||||
let literal = defaults[ordinal].clone();
|
||||
operands.push(intern(
|
||||
literal.semantic_type(),
|
||||
ExpressionOp::Literal { literal },
|
||||
NodeOutputRef {
|
||||
node: graph.nodes[i].id.clone(),
|
||||
socket: input.name.into(),
|
||||
},
|
||||
None,
|
||||
));
|
||||
operand_provenance.push(None);
|
||||
}
|
||||
}
|
||||
let mut provenances = operand_provenance.into_iter().flatten();
|
||||
let provenance = provenances.next();
|
||||
if provenances.any(|candidate| Some(candidate) != provenance) {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"expression mixes mesh provenance",
|
||||
format!("nodes[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
for (output_ordinal, output) in contracts[i].outputs.iter().enumerate() {
|
||||
let key = contracts[i].key;
|
||||
let op = match key {
|
||||
"not" => ExpressionOp::Not { value: operands[0] },
|
||||
"and" | "or" | "xor" | "xnor" => ExpressionOp::BooleanBinary {
|
||||
operation: match key {
|
||||
"and" => BooleanBinaryOp::And,
|
||||
"or" => BooleanBinaryOp::Or,
|
||||
"xor" => BooleanBinaryOp::Xor,
|
||||
_ => BooleanBinaryOp::Xnor,
|
||||
},
|
||||
left: operands[0],
|
||||
right: operands[1],
|
||||
},
|
||||
"greater_than_f32" | "less_than_f32" | "equals_f32" => ExpressionOp::CompareF32 {
|
||||
operation: if key.starts_with("greater") {
|
||||
CompareOp::GreaterThan
|
||||
} else if key.starts_with("less") {
|
||||
CompareOp::LessThan
|
||||
} else {
|
||||
CompareOp::Equals
|
||||
},
|
||||
left: operands[0],
|
||||
right: operands[1],
|
||||
},
|
||||
"greater_than_u32" | "less_than_u32" | "equals_u32" => ExpressionOp::CompareU32 {
|
||||
operation: if key.starts_with("greater") {
|
||||
CompareOp::GreaterThan
|
||||
} else if key.starts_with("less") {
|
||||
CompareOp::LessThan
|
||||
} else {
|
||||
CompareOp::Equals
|
||||
},
|
||||
left: operands[0],
|
||||
right: operands[1],
|
||||
},
|
||||
k if k.starts_with("separate_vec") => ExpressionOp::VectorProject {
|
||||
vector: operands[0],
|
||||
index: output_ordinal as u8,
|
||||
},
|
||||
k if k.starts_with("combine_vec") => ExpressionOp::VectorConstruct {
|
||||
components: operands.clone(),
|
||||
},
|
||||
k if k.starts_with("separate_mat") => ExpressionOp::MatrixColumn {
|
||||
matrix: operands[0],
|
||||
index: output_ordinal as u8,
|
||||
},
|
||||
k if k.starts_with("combine_mat") => ExpressionOp::MatrixConstruct {
|
||||
columns: operands.clone(),
|
||||
},
|
||||
"separate_u32x16" => ExpressionOp::TypeWord {
|
||||
value: operands[0],
|
||||
index: output_ordinal as u8,
|
||||
},
|
||||
"combine_u32x16" => ExpressionOp::TypeConstruct {
|
||||
words: operands.clone(),
|
||||
},
|
||||
"separate_u32_bits" => ExpressionOp::U32Bit {
|
||||
value: operands[0],
|
||||
index: output_ordinal as u8,
|
||||
},
|
||||
"combine_u32_bits" => ExpressionOp::U32Construct {
|
||||
bits: operands.clone(),
|
||||
},
|
||||
"separate_local_aabb" if output_ordinal == 0 => {
|
||||
ExpressionOp::AabbMin { aabb: operands[0] }
|
||||
}
|
||||
"separate_local_aabb" => ExpressionOp::AabbMax { aabb: operands[0] },
|
||||
"frustum_cull" => {
|
||||
requires_camera = true;
|
||||
let mesh = output_ids[&bound[i]["mesh"].producer];
|
||||
if mesh_root.is_some_and(|root| root != mesh) {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"instance traversal has multiple mesh roots",
|
||||
format!("nodes[{i}].inputs.mesh"),
|
||||
));
|
||||
}
|
||||
mesh_root = Some(mesh);
|
||||
ExpressionOp::FrustumCulled {
|
||||
mesh,
|
||||
local_aabb: operands[0],
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let id = intern(
|
||||
output.semantic_type,
|
||||
op,
|
||||
NodeOutputRef {
|
||||
node: graph.nodes[i].id.clone(),
|
||||
socket: output.name.into(),
|
||||
},
|
||||
provenance,
|
||||
);
|
||||
expression_ids.insert(OutputKey(i, output_ordinal as u16), id);
|
||||
expression_provenance.insert(OutputKey(i, output_ordinal as u16), provenance);
|
||||
}
|
||||
}
|
||||
let mut predicates = Vec::new();
|
||||
for (execution, compiled) in executions.iter().enumerate() {
|
||||
let node = compiled.original_node_index as usize;
|
||||
if contracts[node].key != "pipeline" {
|
||||
continue;
|
||||
}
|
||||
let mesh = output_ids[&bound[node]["mesh"].producer];
|
||||
if mesh_root.is_some_and(|root| root != mesh) {
|
||||
return Err(error(
|
||||
"GRAPH_SOCKET_TYPE_MISMATCH",
|
||||
"instance traversal has multiple mesh roots",
|
||||
format!("nodes[{node}].inputs.mesh"),
|
||||
));
|
||||
}
|
||||
mesh_root = Some(mesh);
|
||||
let predicate = if let Some(binding) = bound[node].get("predicate") {
|
||||
expression_ids[&binding.producer]
|
||||
} else {
|
||||
let NormalizedParameters::Pipeline {
|
||||
predicate_default, ..
|
||||
} = params[node]
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
intern(
|
||||
SemanticType::Bool,
|
||||
ExpressionOp::Literal {
|
||||
literal: TypedLiteral::Bool(predicate_default),
|
||||
},
|
||||
NodeOutputRef {
|
||||
node: graph.nodes[node].id.clone(),
|
||||
socket: "predicate".into(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
};
|
||||
predicates.push(PipelinePredicatePlan {
|
||||
execution: execution as u32,
|
||||
predicate,
|
||||
ordinal: 0,
|
||||
});
|
||||
}
|
||||
for (ordinal, predicate) in predicates.iter_mut().enumerate() {
|
||||
predicate.ordinal = ordinal as u32;
|
||||
}
|
||||
if expression_plan.expressions.len() > MAX_EXPRESSIONS
|
||||
|| predicates.len() > MAX_PREDICATE_PIPELINES
|
||||
{
|
||||
return Err(error(
|
||||
"GRAPH_LIMIT_EXCEEDED",
|
||||
"instance traversal plan exceeds limits",
|
||||
"nodes",
|
||||
));
|
||||
}
|
||||
let instance_traversal = mesh_root.map(|mesh| InstanceTraversalPlan {
|
||||
mesh,
|
||||
expressions: expression_plan,
|
||||
pipelines: predicates,
|
||||
requires_camera,
|
||||
});
|
||||
for (ordinal, e) in executions.iter().enumerate() {
|
||||
for o in &e.outputs {
|
||||
resources[o.resource as usize].producer_execution = Some(ordinal as u32);
|
||||
@@ -1917,6 +2123,7 @@ pub fn compile(graph: Graph) -> Result<CompiledGraph, GraphError> {
|
||||
culled_node_count: (graph.nodes.len() - live.len()) as u32,
|
||||
culled_resource_count: (all_outputs - output_ids.len()) as u32,
|
||||
transient_slot_count: transient,
|
||||
instance_traversal,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
use super::MeshFlag;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SemanticType {
|
||||
MeshData,
|
||||
Texture,
|
||||
LocalAabbBuffer,
|
||||
BooleanFlagBuffer,
|
||||
PipelineIndexStream,
|
||||
PipelineActivation,
|
||||
DrawStream,
|
||||
Bool,
|
||||
F32,
|
||||
U32,
|
||||
Vec2,
|
||||
Vec3,
|
||||
Vec4,
|
||||
Mat2,
|
||||
Mat3,
|
||||
Mat4,
|
||||
U32x16,
|
||||
LocalAabb,
|
||||
}
|
||||
|
||||
impl SemanticType {
|
||||
pub const fn is_virtual(self) -> bool {
|
||||
!matches!(self, Self::MeshData | Self::Texture)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecutionClass {
|
||||
Source,
|
||||
CpuPreparation,
|
||||
Compute,
|
||||
Expression,
|
||||
Render,
|
||||
Frame,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FullscreenPolicy {
|
||||
Copy,
|
||||
@@ -29,21 +37,18 @@ pub enum FullscreenPolicy {
|
||||
BloomExtract,
|
||||
BloomComposite,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InputCardinality {
|
||||
RequiredOne,
|
||||
OptionalOne,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(tag = "kind", content = "types", rename_all = "snake_case")]
|
||||
pub enum TypeConstraint {
|
||||
Exact(SemanticType),
|
||||
OneOf(&'static [SemanticType]),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum InputRole {
|
||||
@@ -54,15 +59,8 @@ pub enum InputRole {
|
||||
SampledTexture,
|
||||
ColorTarget { location: u32 },
|
||||
DepthTarget,
|
||||
Expression,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum OutputMetadata {
|
||||
None,
|
||||
BooleanFlag { flag: MeshFlag },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InputSocketContract {
|
||||
@@ -71,15 +69,12 @@ pub struct InputSocketContract {
|
||||
pub cardinality: InputCardinality,
|
||||
pub role: InputRole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OutputSocketContract {
|
||||
pub name: &'static str,
|
||||
pub semantic_type: SemanticType,
|
||||
pub metadata: OutputMetadata,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contract {
|
||||
@@ -94,329 +89,294 @@ pub struct Contract {
|
||||
}
|
||||
|
||||
use SemanticType::*;
|
||||
|
||||
const fn input(
|
||||
const R: InputCardinality = InputCardinality::RequiredOne;
|
||||
const O: InputCardinality = InputCardinality::OptionalOne;
|
||||
const fn i(
|
||||
name: &'static str,
|
||||
accepted: TypeConstraint,
|
||||
ty: SemanticType,
|
||||
cardinality: InputCardinality,
|
||||
role: InputRole,
|
||||
) -> InputSocketContract {
|
||||
InputSocketContract {
|
||||
name,
|
||||
accepted,
|
||||
accepted: TypeConstraint::Exact(ty),
|
||||
cardinality,
|
||||
role,
|
||||
}
|
||||
}
|
||||
|
||||
const fn output(
|
||||
name: &'static str,
|
||||
semantic_type: SemanticType,
|
||||
metadata: OutputMetadata,
|
||||
) -> OutputSocketContract {
|
||||
const fn o(name: &'static str, semantic_type: SemanticType) -> OutputSocketContract {
|
||||
OutputSocketContract {
|
||||
name,
|
||||
semantic_type,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
const REQUIRED: InputCardinality = InputCardinality::RequiredOne;
|
||||
const OPTIONAL: InputCardinality = InputCardinality::OptionalOne;
|
||||
const NONE_IN: &[InputSocketContract] = &[];
|
||||
const NONE_OUT: &[OutputSocketContract] = &[];
|
||||
const TEXTURE_OUT: &[OutputSocketContract] = &[output("texture", Texture, OutputMetadata::None)];
|
||||
const MESH_OUT: &[OutputSocketContract] = &[
|
||||
output("mesh", MeshData, OutputMetadata::None),
|
||||
output("localAabbs", LocalAabbBuffer, OutputMetadata::None),
|
||||
output(
|
||||
"isVisible",
|
||||
BooleanFlagBuffer,
|
||||
OutputMetadata::BooleanFlag {
|
||||
flag: MeshFlag::IsVisible,
|
||||
},
|
||||
),
|
||||
output("pipelineIndices", PipelineIndexStream, OutputMetadata::None),
|
||||
const NONE_I: &[InputSocketContract] = &[];
|
||||
const NONE_O: &[OutputSocketContract] = &[];
|
||||
const MESH_O: &[OutputSocketContract] = &[
|
||||
o("mesh", MeshData),
|
||||
o("type", U32x16),
|
||||
o("localAabb", LocalAabb),
|
||||
];
|
||||
const CULLED_OUT: &[OutputSocketContract] = &[output(
|
||||
"isFrustumCulled",
|
||||
BooleanFlagBuffer,
|
||||
OutputMetadata::BooleanFlag {
|
||||
flag: MeshFlag::IsFrustumCulled,
|
||||
},
|
||||
)];
|
||||
const DRAW_OUT: &[OutputSocketContract] = &[output("draws", DrawStream, OutputMetadata::None)];
|
||||
const ACTIVATION_OUT: &[OutputSocketContract] = &[output(
|
||||
"activation",
|
||||
PipelineActivation,
|
||||
OutputMetadata::None,
|
||||
)];
|
||||
const PIPELINE_OUT: &[OutputSocketContract] = &[
|
||||
output("color", Texture, OutputMetadata::None),
|
||||
output("depth", Texture, OutputMetadata::None),
|
||||
];
|
||||
const FULLSCREEN_COPY_OUT: &[OutputSocketContract] =
|
||||
&[output("color", Texture, OutputMetadata::None)];
|
||||
const CULL_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"mesh",
|
||||
TypeConstraint::Exact(MeshData),
|
||||
REQUIRED,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
input(
|
||||
"localAabbs",
|
||||
TypeConstraint::Exact(LocalAabbBuffer),
|
||||
REQUIRED,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
];
|
||||
const QUERY_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"mesh",
|
||||
TypeConstraint::Exact(MeshData),
|
||||
REQUIRED,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
input(
|
||||
"isVisible",
|
||||
TypeConstraint::Exact(BooleanFlagBuffer),
|
||||
OPTIONAL,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
input(
|
||||
"isFrustumCulled",
|
||||
TypeConstraint::Exact(BooleanFlagBuffer),
|
||||
OPTIONAL,
|
||||
InputRole::StorageRead,
|
||||
),
|
||||
];
|
||||
const REGISTRY_IN: &[InputSocketContract] = &[input(
|
||||
"pipelineIndices",
|
||||
TypeConstraint::Exact(PipelineIndexStream),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
)];
|
||||
const PIPELINE_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"mesh",
|
||||
TypeConstraint::Exact(MeshData),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
),
|
||||
input(
|
||||
"draws",
|
||||
TypeConstraint::Exact(DrawStream),
|
||||
REQUIRED,
|
||||
InputRole::IndirectRead,
|
||||
),
|
||||
input(
|
||||
"activation",
|
||||
TypeConstraint::Exact(PipelineActivation),
|
||||
REQUIRED,
|
||||
InputRole::SemanticRead,
|
||||
),
|
||||
input(
|
||||
const TEXTURE_O: &[OutputSocketContract] = &[o("texture", Texture)];
|
||||
const PIPE_I: &[InputSocketContract] = &[
|
||||
i("mesh", MeshData, R, InputRole::SemanticRead),
|
||||
i("predicate", Bool, O, InputRole::Expression),
|
||||
i(
|
||||
"colorTarget",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
Texture,
|
||||
R,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
input(
|
||||
"depthTarget",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::DepthTarget,
|
||||
),
|
||||
i("depthTarget", Texture, R, InputRole::DepthTarget),
|
||||
];
|
||||
const FULLSCREEN_COPY_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"source",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::SampledTexture,
|
||||
),
|
||||
input(
|
||||
const PIPE_O: &[OutputSocketContract] = &[o("color", Texture), o("depth", Texture)];
|
||||
const CULL_I: &[InputSocketContract] = &[
|
||||
i("mesh", MeshData, R, InputRole::Expression),
|
||||
i("localAabb", LocalAabb, R, InputRole::Expression),
|
||||
];
|
||||
const CULL_O: &[OutputSocketContract] = &[o("isFrustumCulled", Bool)];
|
||||
const COPY_I: &[InputSocketContract] = &[
|
||||
i("source", Texture, R, InputRole::SampledTexture),
|
||||
i(
|
||||
"colorTarget",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
Texture,
|
||||
R,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
];
|
||||
const BLOOM_COMPOSITE_IN: &[InputSocketContract] = &[
|
||||
input(
|
||||
"source",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::SampledTexture,
|
||||
),
|
||||
input(
|
||||
"bloom",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::SampledTexture,
|
||||
),
|
||||
input(
|
||||
const BLOOM_I: &[InputSocketContract] = &[
|
||||
i("source", Texture, R, InputRole::SampledTexture),
|
||||
i("bloom", Texture, R, InputRole::SampledTexture),
|
||||
i(
|
||||
"colorTarget",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
Texture,
|
||||
R,
|
||||
InputRole::ColorTarget { location: 0 },
|
||||
),
|
||||
];
|
||||
const FRAME_OUT_IN: &[InputSocketContract] = &[input(
|
||||
"color",
|
||||
TypeConstraint::Exact(Texture),
|
||||
REQUIRED,
|
||||
InputRole::SampledTexture,
|
||||
)];
|
||||
const COLOR_O: &[OutputSocketContract] = &[o("color", Texture)];
|
||||
const FRAME_I: &[InputSocketContract] = &[i("color", Texture, R, InputRole::SampledTexture)];
|
||||
macro_rules! ins { ($($n:literal:$t:ident),*) => { &[$(i($n,$t,O,InputRole::Expression)),*] } }
|
||||
macro_rules! outs { ($($n:literal:$t:ident),*) => { &[$(o($n,$t)),*] } }
|
||||
macro_rules! c {
|
||||
($k:literal,$v:expr,$e:ident,$ins:expr,$outs:expr,$obs:expr,$policy:expr) => {
|
||||
Contract {
|
||||
key: $k,
|
||||
version: $v,
|
||||
execution: ExecutionClass::$e,
|
||||
inputs: $ins,
|
||||
outputs: $outs,
|
||||
inherently_observable: $obs,
|
||||
fullscreen_policy: $policy,
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! ex {
|
||||
($k:literal,$ins:expr,$outs:expr) => {
|
||||
c!($k, 1, Expression, $ins, $outs, false, None)
|
||||
};
|
||||
}
|
||||
|
||||
pub static CONTRACTS: &[Contract] = &[
|
||||
Contract {
|
||||
key: "mesh",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: MESH_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "texture",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Source,
|
||||
inputs: NONE_IN,
|
||||
outputs: TEXTURE_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "frustum_cull",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Compute,
|
||||
inputs: CULL_IN,
|
||||
outputs: CULLED_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "mesh_query",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Compute,
|
||||
inputs: QUERY_IN,
|
||||
outputs: DRAW_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "pipeline_registry",
|
||||
version: 1,
|
||||
execution: ExecutionClass::CpuPreparation,
|
||||
inputs: REGISTRY_IN,
|
||||
outputs: ACTIVATION_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "pipeline",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: PIPELINE_IN,
|
||||
outputs: PIPELINE_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
Contract {
|
||||
key: "fullscreen_copy",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::Copy),
|
||||
},
|
||||
Contract {
|
||||
key: "color_balance",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "exposure_contrast",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "saturation",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "channel_mixer",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_extract",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::BloomExtract),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_blur",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "bloom_composite",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: BLOOM_COMPOSITE_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::BloomComposite),
|
||||
},
|
||||
Contract {
|
||||
key: "luminance_edge",
|
||||
version: 1,
|
||||
execution: ExecutionClass::Render,
|
||||
inputs: FULLSCREEN_COPY_IN,
|
||||
outputs: FULLSCREEN_COPY_OUT,
|
||||
inherently_observable: false,
|
||||
fullscreen_policy: Some(FullscreenPolicy::HdrSameExtent),
|
||||
},
|
||||
Contract {
|
||||
key: "frame_out",
|
||||
version: 3,
|
||||
execution: ExecutionClass::Frame,
|
||||
inputs: FRAME_OUT_IN,
|
||||
outputs: NONE_OUT,
|
||||
inherently_observable: true,
|
||||
fullscreen_policy: None,
|
||||
},
|
||||
c!("mesh", 2, Source, NONE_I, MESH_O, false, None),
|
||||
c!("texture", 1, Source, NONE_I, TEXTURE_O, false, None),
|
||||
c!("frustum_cull", 2, Expression, CULL_I, CULL_O, false, None),
|
||||
c!("pipeline", 2, Render, PIPE_I, PIPE_O, false, None),
|
||||
ex!("and", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||
ex!("or", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||
ex!("not", ins!("operand":Bool), outs!("value":Bool)),
|
||||
ex!("xor", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||
ex!("xnor", ins!("left":Bool,"right":Bool), outs!("value":Bool)),
|
||||
ex!(
|
||||
"greater_than_f32",
|
||||
ins!("left":F32,"right":F32),
|
||||
outs!("value":Bool)
|
||||
),
|
||||
ex!(
|
||||
"less_than_f32",
|
||||
ins!("left":F32,"right":F32),
|
||||
outs!("value":Bool)
|
||||
),
|
||||
ex!(
|
||||
"equals_f32",
|
||||
ins!("left":F32,"right":F32),
|
||||
outs!("value":Bool)
|
||||
),
|
||||
ex!(
|
||||
"greater_than_u32",
|
||||
ins!("left":U32,"right":U32),
|
||||
outs!("value":Bool)
|
||||
),
|
||||
ex!(
|
||||
"less_than_u32",
|
||||
ins!("left":U32,"right":U32),
|
||||
outs!("value":Bool)
|
||||
),
|
||||
ex!(
|
||||
"equals_u32",
|
||||
ins!("left":U32,"right":U32),
|
||||
outs!("value":Bool)
|
||||
),
|
||||
ex!("separate_vec2", ins!("vector":Vec2), outs!("x":F32,"y":F32)),
|
||||
ex!("combine_vec2", ins!("x":F32,"y":F32), outs!("vector":Vec2)),
|
||||
ex!(
|
||||
"separate_vec3",
|
||||
ins!("vector":Vec3),
|
||||
outs!("x":F32,"y":F32,"z":F32)
|
||||
),
|
||||
ex!(
|
||||
"combine_vec3",
|
||||
ins!("x":F32,"y":F32,"z":F32),
|
||||
outs!("vector":Vec3)
|
||||
),
|
||||
ex!(
|
||||
"separate_vec4",
|
||||
ins!("vector":Vec4),
|
||||
outs!("x":F32,"y":F32,"z":F32,"w":F32)
|
||||
),
|
||||
ex!(
|
||||
"combine_vec4",
|
||||
ins!("x":F32,"y":F32,"z":F32,"w":F32),
|
||||
outs!("vector":Vec4)
|
||||
),
|
||||
ex!(
|
||||
"separate_mat2",
|
||||
ins!("matrix":Mat2),
|
||||
outs!("column0":Vec2,"column1":Vec2)
|
||||
),
|
||||
ex!(
|
||||
"combine_mat2",
|
||||
ins!("column0":Vec2,"column1":Vec2),
|
||||
outs!("matrix":Mat2)
|
||||
),
|
||||
ex!(
|
||||
"separate_mat3",
|
||||
ins!("matrix":Mat3),
|
||||
outs!("column0":Vec3,"column1":Vec3,"column2":Vec3)
|
||||
),
|
||||
ex!(
|
||||
"combine_mat3",
|
||||
ins!("column0":Vec3,"column1":Vec3,"column2":Vec3),
|
||||
outs!("matrix":Mat3)
|
||||
),
|
||||
ex!(
|
||||
"separate_mat4",
|
||||
ins!("matrix":Mat4),
|
||||
outs!("column0":Vec4,"column1":Vec4,"column2":Vec4,"column3":Vec4)
|
||||
),
|
||||
ex!(
|
||||
"combine_mat4",
|
||||
ins!("column0":Vec4,"column1":Vec4,"column2":Vec4,"column3":Vec4),
|
||||
outs!("matrix":Mat4)
|
||||
),
|
||||
ex!(
|
||||
"separate_u32x16",
|
||||
ins!("value":U32x16),
|
||||
outs!("word0":U32,"word1":U32,"word2":U32,"word3":U32,"word4":U32,"word5":U32,"word6":U32,"word7":U32,"word8":U32,"word9":U32,"word10":U32,"word11":U32,"word12":U32,"word13":U32,"word14":U32,"word15":U32)
|
||||
),
|
||||
ex!(
|
||||
"combine_u32x16",
|
||||
ins!("word0":U32,"word1":U32,"word2":U32,"word3":U32,"word4":U32,"word5":U32,"word6":U32,"word7":U32,"word8":U32,"word9":U32,"word10":U32,"word11":U32,"word12":U32,"word13":U32,"word14":U32,"word15":U32),
|
||||
outs!("value":U32x16)
|
||||
),
|
||||
ex!(
|
||||
"separate_u32_bits",
|
||||
ins!("value":U32),
|
||||
outs!("bit0":Bool,"bit1":Bool,"bit2":Bool,"bit3":Bool,"bit4":Bool,"bit5":Bool,"bit6":Bool,"bit7":Bool,"bit8":Bool,"bit9":Bool,"bit10":Bool,"bit11":Bool,"bit12":Bool,"bit13":Bool,"bit14":Bool,"bit15":Bool,"bit16":Bool,"bit17":Bool,"bit18":Bool,"bit19":Bool,"bit20":Bool,"bit21":Bool,"bit22":Bool,"bit23":Bool,"bit24":Bool,"bit25":Bool,"bit26":Bool,"bit27":Bool,"bit28":Bool,"bit29":Bool,"bit30":Bool,"bit31":Bool)
|
||||
),
|
||||
ex!(
|
||||
"combine_u32_bits",
|
||||
ins!("bit0":Bool,"bit1":Bool,"bit2":Bool,"bit3":Bool,"bit4":Bool,"bit5":Bool,"bit6":Bool,"bit7":Bool,"bit8":Bool,"bit9":Bool,"bit10":Bool,"bit11":Bool,"bit12":Bool,"bit13":Bool,"bit14":Bool,"bit15":Bool,"bit16":Bool,"bit17":Bool,"bit18":Bool,"bit19":Bool,"bit20":Bool,"bit21":Bool,"bit22":Bool,"bit23":Bool,"bit24":Bool,"bit25":Bool,"bit26":Bool,"bit27":Bool,"bit28":Bool,"bit29":Bool,"bit30":Bool,"bit31":Bool),
|
||||
outs!("value":U32)
|
||||
),
|
||||
ex!(
|
||||
"separate_local_aabb",
|
||||
ins!("value":LocalAabb),
|
||||
outs!("min":Vec3,"max":Vec3)
|
||||
),
|
||||
c!(
|
||||
"fullscreen_copy",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::Copy)
|
||||
),
|
||||
c!(
|
||||
"color_balance",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::HdrSameExtent)
|
||||
),
|
||||
c!(
|
||||
"exposure_contrast",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::HdrSameExtent)
|
||||
),
|
||||
c!(
|
||||
"saturation",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::HdrSameExtent)
|
||||
),
|
||||
c!(
|
||||
"channel_mixer",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::HdrSameExtent)
|
||||
),
|
||||
c!(
|
||||
"bloom_extract",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::BloomExtract)
|
||||
),
|
||||
c!(
|
||||
"bloom_blur",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::HdrSameExtent)
|
||||
),
|
||||
c!(
|
||||
"bloom_composite",
|
||||
1,
|
||||
Render,
|
||||
BLOOM_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::BloomComposite)
|
||||
),
|
||||
c!(
|
||||
"luminance_edge",
|
||||
1,
|
||||
Render,
|
||||
COPY_I,
|
||||
COLOR_O,
|
||||
false,
|
||||
Some(FullscreenPolicy::HdrSameExtent)
|
||||
),
|
||||
c!("frame_out", 3, Frame, FRAME_I, NONE_O, true, None),
|
||||
];
|
||||
|
||||
pub fn contract(key: &str) -> Option<&'static Contract> {
|
||||
CONTRACTS.iter().find(|contract| contract.key == key)
|
||||
CONTRACTS.iter().find(|c| c.key == key)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Typed, device-independent instance predicate IR.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{NodeOutputRef, SemanticType};
|
||||
|
||||
pub const MAX_EXPRESSIONS: usize = 4096;
|
||||
pub const MAX_PREDICATE_PIPELINES: usize = 64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ExprId(pub u32);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
|
||||
pub enum TypedLiteral {
|
||||
Bool(bool),
|
||||
F32(f32),
|
||||
U32(u32),
|
||||
Vec2([f32; 2]),
|
||||
Vec3([f32; 3]),
|
||||
Vec4([f32; 4]),
|
||||
Mat2([[f32; 2]; 2]),
|
||||
Mat3([[f32; 3]; 3]),
|
||||
Mat4([[f32; 4]; 4]),
|
||||
U32x16([u32; 16]),
|
||||
LocalAabb { min: [f32; 3], max: [f32; 3] },
|
||||
}
|
||||
|
||||
impl TypedLiteral {
|
||||
pub fn semantic_type(&self) -> SemanticType {
|
||||
match self {
|
||||
Self::Bool(_) => SemanticType::Bool,
|
||||
Self::F32(_) => SemanticType::F32,
|
||||
Self::U32(_) => SemanticType::U32,
|
||||
Self::Vec2(_) => SemanticType::Vec2,
|
||||
Self::Vec3(_) => SemanticType::Vec3,
|
||||
Self::Vec4(_) => SemanticType::Vec4,
|
||||
Self::Mat2(_) => SemanticType::Mat2,
|
||||
Self::Mat3(_) => SemanticType::Mat3,
|
||||
Self::Mat4(_) => SemanticType::Mat4,
|
||||
Self::U32x16(_) => SemanticType::U32x16,
|
||||
Self::LocalAabb { .. } => SemanticType::LocalAabb,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_finite(&self) -> bool {
|
||||
let finite = |values: &[f32]| values.iter().all(|value| value.is_finite());
|
||||
match self {
|
||||
Self::F32(value) => value.is_finite(),
|
||||
Self::Vec2(value) => finite(value),
|
||||
Self::Vec3(value) => finite(value),
|
||||
Self::Vec4(value) => finite(value),
|
||||
Self::Mat2(value) => value.iter().all(|column| finite(column)),
|
||||
Self::Mat3(value) => value.iter().all(|column| finite(column)),
|
||||
Self::Mat4(value) => value.iter().all(|column| finite(column)),
|
||||
Self::LocalAabb { min, max } => finite(min) && finite(max),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompareOp {
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
Equals,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BooleanBinaryOp {
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
Xnor,
|
||||
}
|
||||
|
||||
/// All operand IDs refer to earlier entries in [`ExpressionPlan::expressions`].
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
pub enum ExpressionOp {
|
||||
Literal {
|
||||
literal: TypedLiteral,
|
||||
},
|
||||
InstanceType {
|
||||
mesh: u32,
|
||||
},
|
||||
LocalAabb {
|
||||
mesh: u32,
|
||||
},
|
||||
Not {
|
||||
value: ExprId,
|
||||
},
|
||||
BooleanBinary {
|
||||
operation: BooleanBinaryOp,
|
||||
left: ExprId,
|
||||
right: ExprId,
|
||||
},
|
||||
CompareF32 {
|
||||
operation: CompareOp,
|
||||
left: ExprId,
|
||||
right: ExprId,
|
||||
},
|
||||
CompareU32 {
|
||||
operation: CompareOp,
|
||||
left: ExprId,
|
||||
right: ExprId,
|
||||
},
|
||||
VectorProject {
|
||||
vector: ExprId,
|
||||
index: u8,
|
||||
},
|
||||
VectorConstruct {
|
||||
components: Vec<ExprId>,
|
||||
},
|
||||
MatrixColumn {
|
||||
matrix: ExprId,
|
||||
index: u8,
|
||||
},
|
||||
MatrixConstruct {
|
||||
columns: Vec<ExprId>,
|
||||
},
|
||||
TypeWord {
|
||||
value: ExprId,
|
||||
index: u8,
|
||||
},
|
||||
TypeConstruct {
|
||||
words: Vec<ExprId>,
|
||||
},
|
||||
U32Bit {
|
||||
value: ExprId,
|
||||
index: u8,
|
||||
},
|
||||
U32Construct {
|
||||
bits: Vec<ExprId>,
|
||||
},
|
||||
AabbMin {
|
||||
aabb: ExprId,
|
||||
},
|
||||
AabbMax {
|
||||
aabb: ExprId,
|
||||
},
|
||||
FrustumCulled {
|
||||
mesh: u32,
|
||||
local_aabb: ExprId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Expression {
|
||||
pub semantic_type: SemanticType,
|
||||
pub op: ExpressionOp,
|
||||
pub origin: NodeOutputRef,
|
||||
pub mesh_provenance: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExpressionPlan {
|
||||
pub expressions: Vec<Expression>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PipelinePredicatePlan {
|
||||
pub execution: u32,
|
||||
pub predicate: ExprId,
|
||||
pub ordinal: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstanceTraversalPlan {
|
||||
pub mesh: u32,
|
||||
pub expressions: ExpressionPlan,
|
||||
pub pipelines: Vec<PipelinePredicatePlan>,
|
||||
pub requires_camera: bool,
|
||||
}
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
mod compiler;
|
||||
mod contracts;
|
||||
mod expression;
|
||||
mod plan;
|
||||
mod registry;
|
||||
mod runtime;
|
||||
mod schema;
|
||||
|
||||
pub use compiler::{compile, mesh_predicate_matches, parse_and_compile};
|
||||
pub use compiler::{compile, parse_and_compile};
|
||||
pub use contracts::*;
|
||||
pub use expression::*;
|
||||
pub use plan::*;
|
||||
pub use registry::{CompiledGraphId, Registry};
|
||||
pub use runtime::*;
|
||||
|
||||
@@ -16,6 +16,8 @@ pub struct CompiledGraph {
|
||||
pub culled_node_count: u32,
|
||||
pub culled_resource_count: u32,
|
||||
pub transient_slot_count: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub instance_traversal: Option<InstanceTraversalPlan>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -47,22 +49,6 @@ pub enum ResourcePlan {
|
||||
allocation: Option<AllocationRef>,
|
||||
},
|
||||
MeshData,
|
||||
LocalAabbBuffer {
|
||||
mesh: u32,
|
||||
},
|
||||
BooleanFlagBuffer {
|
||||
mesh: u32,
|
||||
flag: MeshFlag,
|
||||
},
|
||||
PipelineIndexStream {
|
||||
mesh: u32,
|
||||
},
|
||||
PipelineActivation {
|
||||
pipeline_indices: u32,
|
||||
},
|
||||
DrawStream {
|
||||
mesh: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -95,10 +81,6 @@ pub struct CompiledSocketOutput {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ExecutionKind {
|
||||
CpuPreparation,
|
||||
Compute {
|
||||
work: ComputeWork,
|
||||
},
|
||||
Render {
|
||||
color_attachments: Vec<ColorAttachmentPlan>,
|
||||
depth_stencil: Option<DepthStencilAttachmentPlan>,
|
||||
@@ -108,13 +90,6 @@ pub enum ExecutionKind {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ComputeWork {
|
||||
FrustumCull,
|
||||
MeshQuery,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ColorAttachmentPlan {
|
||||
@@ -196,17 +171,16 @@ pub enum NormalizedParameters {
|
||||
FrustumCull {
|
||||
camera: ActiveCamera,
|
||||
},
|
||||
MeshQuery {
|
||||
visible_predicate: RuntimePredicate,
|
||||
frustum_culled_predicate: RuntimePredicate,
|
||||
ExpressionDefaults {
|
||||
defaults: Vec<TypedLiteral>,
|
||||
},
|
||||
PipelineRegistry,
|
||||
Pipeline {
|
||||
pipeline: String,
|
||||
depth_compare: CompareFunction,
|
||||
depth_write_enabled: bool,
|
||||
clear_depth: f32,
|
||||
clear_color: [f64; 4],
|
||||
predicate_default: bool,
|
||||
},
|
||||
FullscreenCopy,
|
||||
ColorBalance {
|
||||
|
||||
@@ -166,12 +166,6 @@ pub struct RuntimeAllocationClass {
|
||||
pub slots: Vec<RuntimeAllocationSlot>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MeshQueryRuntimeKey {
|
||||
pub visible: RuntimePredicate,
|
||||
pub frustum_culled: RuntimePredicate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeExecution {
|
||||
pub execution: u32,
|
||||
@@ -182,13 +176,13 @@ pub struct RuntimeExecution {
|
||||
pub struct RuntimeAllocationPlan {
|
||||
pub classes: Vec<RuntimeAllocationClass>,
|
||||
pub resource_allocations: Vec<Option<AllocationRef>>,
|
||||
pub query: MeshQueryRuntimeKey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimePlan {
|
||||
pub allocations: RuntimeAllocationPlan,
|
||||
pub executions: Vec<RuntimeExecution>,
|
||||
pub instance_traversal: Option<InstanceTraversalPlan>,
|
||||
pub surface: RuntimeSurfaceContract,
|
||||
}
|
||||
|
||||
@@ -381,11 +375,7 @@ fn valid_pipeline_name(name: &str) -> bool {
|
||||
|
||||
fn execution_supported(key: &str) -> bool {
|
||||
contract(key).is_some_and(|contract| {
|
||||
contract.fullscreen_policy.is_some()
|
||||
|| matches!(
|
||||
key,
|
||||
"frustum_cull" | "mesh_query" | "pipeline_registry" | "pipeline" | "frame_out"
|
||||
)
|
||||
contract.fullscreen_policy.is_some() || matches!(key, "pipeline" | "frame_out")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -396,19 +386,6 @@ fn resource_is_mesh(graph: &CompiledGraph, id: u32) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_exact_producer(
|
||||
graph: &CompiledGraph,
|
||||
consumer: usize,
|
||||
resource: u32,
|
||||
executor: &str,
|
||||
socket: &str,
|
||||
) -> bool {
|
||||
graph.executions[..consumer].iter().any(|execution| {
|
||||
execution.executor.key == executor
|
||||
&& matches!(execution.outputs.as_slice(), [output] if output.socket == socket && output.resource == resource)
|
||||
})
|
||||
}
|
||||
|
||||
fn texture_descriptor<'a>(
|
||||
graph: &'a CompiledGraph,
|
||||
id: u32,
|
||||
@@ -704,181 +681,15 @@ fn validate_fullscreen_execution(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_compute_execution(
|
||||
graph: &CompiledGraph,
|
||||
i: usize,
|
||||
execution: &CompiledExecution,
|
||||
) -> Result<(), GraphError> {
|
||||
let path = |field| format!("executions[{i}].{field}");
|
||||
match execution.executor.key.as_str() {
|
||||
"frustum_cull" => {
|
||||
if !matches!(
|
||||
execution.parameters,
|
||||
NormalizedParameters::FrustumCull {
|
||||
camera: ActiveCamera::Active
|
||||
}
|
||||
) {
|
||||
return Err(invalid(
|
||||
"frustum cull parameters mismatch",
|
||||
path("parameters"),
|
||||
));
|
||||
}
|
||||
if !matches!(
|
||||
execution.kind,
|
||||
ExecutionKind::Compute {
|
||||
work: ComputeWork::FrustumCull
|
||||
}
|
||||
) {
|
||||
return Err(invalid("frustum cull work mismatch", path("kind")));
|
||||
}
|
||||
let [mesh, aabbs] = execution.inputs.as_slice() else {
|
||||
return Err(invalid("frustum cull inputs mismatch", path("inputs")));
|
||||
};
|
||||
let [flags] = execution.outputs.as_slice() else {
|
||||
return Err(invalid("frustum cull outputs mismatch", path("outputs")));
|
||||
};
|
||||
if mesh.socket != "mesh"
|
||||
|| aabbs.socket != "localAabbs"
|
||||
|| flags.socket != "isFrustumCulled"
|
||||
{
|
||||
return Err(invalid(
|
||||
"frustum cull socket order mismatch",
|
||||
path("inputs"),
|
||||
));
|
||||
}
|
||||
if !matches!(execution.accesses.as_slice(),
|
||||
[CompiledAccess { socket: s0, resource: r0, mode: AccessMode::StorageRead },
|
||||
CompiledAccess { socket: s1, resource: r1, mode: AccessMode::StorageRead },
|
||||
CompiledAccess { socket: s2, resource: r2, mode: AccessMode::StorageWrite { full_overwrite: true } }]
|
||||
if s0 == "mesh" && *r0 == mesh.resource && s1 == "localAabbs" && *r1 == aabbs.resource
|
||||
&& s2 == "isFrustumCulled" && *r2 == flags.resource)
|
||||
{
|
||||
return Err(invalid("frustum cull accesses mismatch", path("accesses")));
|
||||
}
|
||||
if !resource_is_mesh(graph, mesh.resource)
|
||||
|| !matches!(graph.resources[aabbs.resource as usize], CompiledResource { semantic_type: SemanticType::LocalAabbBuffer, plan: ResourcePlan::LocalAabbBuffer { mesh: m }, .. } if m == mesh.resource)
|
||||
|| !matches!(graph.resources[flags.resource as usize], CompiledResource { semantic_type: SemanticType::BooleanFlagBuffer, plan: ResourcePlan::BooleanFlagBuffer { mesh: m, flag: MeshFlag::IsFrustumCulled }, .. } if m == mesh.resource)
|
||||
{
|
||||
return Err(invalid(
|
||||
"frustum cull mesh provenance mismatch",
|
||||
path("inputs"),
|
||||
));
|
||||
}
|
||||
}
|
||||
"mesh_query" => {
|
||||
let NormalizedParameters::MeshQuery {
|
||||
visible_predicate,
|
||||
frustum_culled_predicate,
|
||||
} = execution.parameters
|
||||
else {
|
||||
return Err(invalid(
|
||||
"mesh query parameters mismatch",
|
||||
path("parameters"),
|
||||
));
|
||||
};
|
||||
if (visible_predicate == RuntimePredicate::Never)
|
||||
!= (frustum_culled_predicate == RuntimePredicate::Never)
|
||||
{
|
||||
return Err(invalid(
|
||||
"mesh query never predicates must be paired",
|
||||
path("parameters"),
|
||||
));
|
||||
}
|
||||
if !matches!(
|
||||
execution.kind,
|
||||
ExecutionKind::Compute {
|
||||
work: ComputeWork::MeshQuery
|
||||
}
|
||||
) {
|
||||
return Err(invalid("mesh query work mismatch", path("kind")));
|
||||
}
|
||||
let active = |p| {
|
||||
matches!(
|
||||
p,
|
||||
RuntimePredicate::RequiredTrue | RuntimePredicate::RequiredFalse
|
||||
)
|
||||
};
|
||||
let mut sockets = vec!["mesh"];
|
||||
if active(visible_predicate) {
|
||||
sockets.push("isVisible");
|
||||
}
|
||||
if active(frustum_culled_predicate) {
|
||||
sockets.push("isFrustumCulled");
|
||||
}
|
||||
if execution.inputs.len() != sockets.len()
|
||||
|| execution
|
||||
.inputs
|
||||
.iter()
|
||||
.zip(&sockets)
|
||||
.any(|(v, s)| v.socket != *s)
|
||||
|| !matches!(execution.outputs.as_slice(), [CompiledSocketOutput { socket, .. }] if socket == "draws")
|
||||
{
|
||||
return Err(invalid("mesh query socket order mismatch", path("inputs")));
|
||||
}
|
||||
let output = execution.outputs[0].resource;
|
||||
if execution.accesses.len() != sockets.len() + 1
|
||||
|| execution
|
||||
.inputs
|
||||
.iter()
|
||||
.zip(&execution.accesses)
|
||||
.any(|(input, access)| {
|
||||
access.socket != input.socket
|
||||
|| access.resource != input.resource
|
||||
|| !matches!(access.mode, AccessMode::StorageRead)
|
||||
})
|
||||
|| !matches!(&execution.accesses[sockets.len()], CompiledAccess { socket, resource, mode: AccessMode::StorageWrite { full_overwrite: true } } if socket == "draws" && *resource == output)
|
||||
{
|
||||
return Err(invalid("mesh query accesses mismatch", path("accesses")));
|
||||
}
|
||||
let mesh = execution.inputs[0].resource;
|
||||
if !resource_is_mesh(graph, mesh) {
|
||||
return Err(invalid(
|
||||
"mesh query mesh provenance mismatch",
|
||||
path("inputs"),
|
||||
));
|
||||
}
|
||||
for input in execution.inputs.iter().skip(1) {
|
||||
let flag = if input.socket == "isVisible" {
|
||||
MeshFlag::IsVisible
|
||||
} else {
|
||||
MeshFlag::IsFrustumCulled
|
||||
};
|
||||
if !matches!(graph.resources[input.resource as usize], CompiledResource { semantic_type: SemanticType::BooleanFlagBuffer, plan: ResourcePlan::BooleanFlagBuffer { mesh: m, flag: f }, .. } if m == mesh && f == flag)
|
||||
{
|
||||
return Err(invalid(
|
||||
"mesh query flag provenance mismatch",
|
||||
path("inputs"),
|
||||
));
|
||||
}
|
||||
if flag == MeshFlag::IsFrustumCulled
|
||||
&& !has_exact_producer(
|
||||
graph,
|
||||
i,
|
||||
input.resource,
|
||||
"frustum_cull",
|
||||
"isFrustumCulled",
|
||||
)
|
||||
{
|
||||
return Err(invalid(
|
||||
"mesh query frustum flag producer mismatch",
|
||||
path("inputs"),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !matches!(graph.resources[output as usize], CompiledResource { semantic_type: SemanticType::DrawStream, plan: ResourcePlan::DrawStream { mesh: m }, .. } if m == mesh)
|
||||
{
|
||||
return Err(invalid(
|
||||
"mesh query output provenance mismatch",
|
||||
path("outputs"),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
for (i, resource) in graph.resources.iter().enumerate() {
|
||||
if resource.semantic_type.is_virtual() {
|
||||
return Err(invalid(
|
||||
"virtual semantic type was materialized",
|
||||
format!("resources[{i}].semanticType"),
|
||||
));
|
||||
}
|
||||
}
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
if !execution_supported(&execution.executor.key) {
|
||||
return Err(error(
|
||||
@@ -939,7 +750,6 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
}
|
||||
referenced.insert(access.resource);
|
||||
}
|
||||
validate_compute_execution(graph, i, execution)?;
|
||||
validate_fullscreen_execution(graph, i, execution, contract)?;
|
||||
for resource in referenced {
|
||||
uses.get_mut(resource as usize)
|
||||
@@ -1071,6 +881,291 @@ fn validate_canonical_plan(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_instance_traversal(graph: &CompiledGraph) -> Result<(), GraphError> {
|
||||
let pipeline_indices: Vec<_> = graph
|
||||
.executions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, e)| (e.executor.key == "pipeline").then_some(i as u32))
|
||||
.collect();
|
||||
let Some(plan) = &graph.instance_traversal else {
|
||||
return if pipeline_indices.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(invalid(
|
||||
"live pipelines require instance traversal",
|
||||
"instanceTraversal",
|
||||
))
|
||||
};
|
||||
};
|
||||
if pipeline_indices.is_empty() {
|
||||
return Err(invalid(
|
||||
"instance traversal has no live pipelines",
|
||||
"instanceTraversal",
|
||||
));
|
||||
}
|
||||
if !resource_is_mesh(graph, plan.mesh) {
|
||||
return Err(invalid(
|
||||
"traversal mesh is invalid",
|
||||
"instanceTraversal.mesh",
|
||||
));
|
||||
}
|
||||
let expressions = &plan.expressions.expressions;
|
||||
if expressions.len() > MAX_EXPRESSIONS || plan.pipelines.len() > MAX_PREDICATE_PIPELINES {
|
||||
return Err(invalid(
|
||||
"instance traversal exceeds runtime limits",
|
||||
"instanceTraversal",
|
||||
));
|
||||
}
|
||||
let ty = |id: ExprId| expressions.get(id.0 as usize).map(|e| e.semantic_type);
|
||||
for (i, expression) in expressions.iter().enumerate() {
|
||||
let path = format!("instanceTraversal.expressions.expressions[{i}]");
|
||||
let ids: Vec<ExprId> = match &expression.op {
|
||||
ExpressionOp::Literal { literal } => {
|
||||
if literal.semantic_type() != expression.semantic_type || !literal.is_finite() {
|
||||
return Err(invalid("literal type or value is invalid", &path));
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
ExpressionOp::InstanceType { mesh } => {
|
||||
if expression.semantic_type != SemanticType::U32x16 || *mesh != plan.mesh {
|
||||
return Err(invalid("instance type signature is invalid", &path));
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
ExpressionOp::LocalAabb { mesh } => {
|
||||
if expression.semantic_type != SemanticType::LocalAabb || *mesh != plan.mesh {
|
||||
return Err(invalid("local aabb signature is invalid", &path));
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
ExpressionOp::Not { value } => {
|
||||
if expression.semantic_type != SemanticType::Bool
|
||||
|| ty(*value) != Some(SemanticType::Bool)
|
||||
{
|
||||
return Err(invalid("not signature is invalid", &path));
|
||||
}
|
||||
vec![*value]
|
||||
}
|
||||
ExpressionOp::BooleanBinary { left, right, .. } => {
|
||||
if expression.semantic_type != SemanticType::Bool
|
||||
|| ty(*left) != Some(SemanticType::Bool)
|
||||
|| ty(*right) != Some(SemanticType::Bool)
|
||||
{
|
||||
return Err(invalid("boolean signature is invalid", &path));
|
||||
}
|
||||
vec![*left, *right]
|
||||
}
|
||||
ExpressionOp::CompareF32 { left, right, .. } => {
|
||||
if expression.semantic_type != SemanticType::Bool
|
||||
|| ty(*left) != Some(SemanticType::F32)
|
||||
|| ty(*right) != Some(SemanticType::F32)
|
||||
{
|
||||
return Err(invalid("f32 comparison signature is invalid", &path));
|
||||
}
|
||||
vec![*left, *right]
|
||||
}
|
||||
ExpressionOp::CompareU32 { left, right, .. } => {
|
||||
if expression.semantic_type != SemanticType::Bool
|
||||
|| ty(*left) != Some(SemanticType::U32)
|
||||
|| ty(*right) != Some(SemanticType::U32)
|
||||
{
|
||||
return Err(invalid("u32 comparison signature is invalid", &path));
|
||||
}
|
||||
vec![*left, *right]
|
||||
}
|
||||
ExpressionOp::VectorProject { vector, index } => {
|
||||
let n = match ty(*vector) {
|
||||
Some(SemanticType::Vec2) => 2,
|
||||
Some(SemanticType::Vec3) => 3,
|
||||
Some(SemanticType::Vec4) => 4,
|
||||
_ => 0,
|
||||
};
|
||||
if expression.semantic_type != SemanticType::F32 || usize::from(*index) >= n {
|
||||
return Err(invalid("vector projection signature is invalid", &path));
|
||||
}
|
||||
vec![*vector]
|
||||
}
|
||||
ExpressionOp::VectorConstruct { components } => {
|
||||
let n = match expression.semantic_type {
|
||||
SemanticType::Vec2 => 2,
|
||||
SemanticType::Vec3 => 3,
|
||||
SemanticType::Vec4 => 4,
|
||||
_ => 0,
|
||||
};
|
||||
if components.len() != n
|
||||
|| components
|
||||
.iter()
|
||||
.any(|id| ty(*id) != Some(SemanticType::F32))
|
||||
{
|
||||
return Err(invalid("vector constructor signature is invalid", &path));
|
||||
}
|
||||
components.clone()
|
||||
}
|
||||
ExpressionOp::MatrixColumn { matrix, index } => {
|
||||
let (n, out) = match ty(*matrix) {
|
||||
Some(SemanticType::Mat2) => (2, SemanticType::Vec2),
|
||||
Some(SemanticType::Mat3) => (3, SemanticType::Vec3),
|
||||
Some(SemanticType::Mat4) => (4, SemanticType::Vec4),
|
||||
_ => (0, SemanticType::Bool),
|
||||
};
|
||||
if usize::from(*index) >= n || expression.semantic_type != out {
|
||||
return Err(invalid("matrix column signature is invalid", &path));
|
||||
}
|
||||
vec![*matrix]
|
||||
}
|
||||
ExpressionOp::MatrixConstruct { columns } => {
|
||||
let (n, col) = match expression.semantic_type {
|
||||
SemanticType::Mat2 => (2, SemanticType::Vec2),
|
||||
SemanticType::Mat3 => (3, SemanticType::Vec3),
|
||||
SemanticType::Mat4 => (4, SemanticType::Vec4),
|
||||
_ => (0, SemanticType::Bool),
|
||||
};
|
||||
if columns.len() != n || columns.iter().any(|id| ty(*id) != Some(col)) {
|
||||
return Err(invalid("matrix constructor signature is invalid", &path));
|
||||
}
|
||||
columns.clone()
|
||||
}
|
||||
ExpressionOp::TypeWord { value, index } => {
|
||||
if expression.semantic_type != SemanticType::U32
|
||||
|| ty(*value) != Some(SemanticType::U32x16)
|
||||
|| *index >= 16
|
||||
{
|
||||
return Err(invalid("type word signature is invalid", &path));
|
||||
}
|
||||
vec![*value]
|
||||
}
|
||||
ExpressionOp::TypeConstruct { words } => {
|
||||
if expression.semantic_type != SemanticType::U32x16
|
||||
|| words.len() != 16
|
||||
|| words.iter().any(|id| ty(*id) != Some(SemanticType::U32))
|
||||
{
|
||||
return Err(invalid("type constructor signature is invalid", &path));
|
||||
}
|
||||
words.clone()
|
||||
}
|
||||
ExpressionOp::U32Bit { value, index } => {
|
||||
if expression.semantic_type != SemanticType::Bool
|
||||
|| ty(*value) != Some(SemanticType::U32)
|
||||
|| *index >= 32
|
||||
{
|
||||
return Err(invalid("bit signature is invalid", &path));
|
||||
}
|
||||
vec![*value]
|
||||
}
|
||||
ExpressionOp::U32Construct { bits } => {
|
||||
if expression.semantic_type != SemanticType::U32
|
||||
|| bits.len() != 32
|
||||
|| bits.iter().any(|id| ty(*id) != Some(SemanticType::Bool))
|
||||
{
|
||||
return Err(invalid("u32 constructor signature is invalid", &path));
|
||||
}
|
||||
bits.clone()
|
||||
}
|
||||
ExpressionOp::AabbMin { aabb } | ExpressionOp::AabbMax { aabb } => {
|
||||
if expression.semantic_type != SemanticType::Vec3
|
||||
|| ty(*aabb) != Some(SemanticType::LocalAabb)
|
||||
{
|
||||
return Err(invalid("aabb projection signature is invalid", &path));
|
||||
}
|
||||
vec![*aabb]
|
||||
}
|
||||
ExpressionOp::FrustumCulled { mesh, local_aabb } => {
|
||||
if expression.semantic_type != SemanticType::Bool
|
||||
|| *mesh != plan.mesh
|
||||
|| ty(*local_aabb) != Some(SemanticType::LocalAabb)
|
||||
|| expressions
|
||||
.get(local_aabb.0 as usize)
|
||||
.and_then(|e| e.mesh_provenance)
|
||||
!= Some(plan.mesh)
|
||||
{
|
||||
return Err(invalid("frustum signature or provenance is invalid", &path));
|
||||
}
|
||||
vec![*local_aabb]
|
||||
}
|
||||
};
|
||||
if ids.iter().any(|id| id.0 as usize >= i) {
|
||||
return Err(invalid("expression operands must precede consumer", &path));
|
||||
}
|
||||
let expected_provenance = match &expression.op {
|
||||
ExpressionOp::Literal { .. } => None,
|
||||
ExpressionOp::InstanceType { .. } | ExpressionOp::LocalAabb { .. } => Some(plan.mesh),
|
||||
_ => {
|
||||
let mut p = ids
|
||||
.iter()
|
||||
.filter_map(|id| expressions[id.0 as usize].mesh_provenance);
|
||||
let first = p.next();
|
||||
if p.any(|v| Some(v) != first) {
|
||||
return Err(invalid("expression mixes mesh provenance", &path));
|
||||
}
|
||||
first
|
||||
}
|
||||
};
|
||||
if expression.mesh_provenance != expected_provenance {
|
||||
return Err(invalid("expression provenance is not canonical", &path));
|
||||
}
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
let mut reachable = vec![false; expressions.len()];
|
||||
for (ordinal, entry) in plan.pipelines.iter().enumerate() {
|
||||
if entry.ordinal as usize != ordinal
|
||||
|| pipeline_indices.get(ordinal) != Some(&entry.execution)
|
||||
|| !seen.insert(entry.execution)
|
||||
|| ty(entry.predicate) != Some(SemanticType::Bool)
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline predicate table is not canonical",
|
||||
format!("instanceTraversal.pipelines[{ordinal}]"),
|
||||
));
|
||||
}
|
||||
let mut stack = vec![entry.predicate];
|
||||
while let Some(id) = stack.pop() {
|
||||
if reachable[id.0 as usize] {
|
||||
continue;
|
||||
}
|
||||
reachable[id.0 as usize] = true;
|
||||
stack.extend(expression_operands(&expressions[id.0 as usize].op));
|
||||
}
|
||||
}
|
||||
if plan.pipelines.len() != pipeline_indices.len() {
|
||||
return Err(invalid(
|
||||
"pipeline predicate table is incomplete",
|
||||
"instanceTraversal.pipelines",
|
||||
));
|
||||
}
|
||||
let requires_camera = expressions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(i, e)| reachable[i] && matches!(e.op, ExpressionOp::FrustumCulled { .. }));
|
||||
if plan.requires_camera != requires_camera {
|
||||
return Err(invalid(
|
||||
"requires_camera is not canonical",
|
||||
"instanceTraversal.requiresCamera",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expression_operands(op: &ExpressionOp) -> Vec<ExprId> {
|
||||
match op {
|
||||
ExpressionOp::Not { value }
|
||||
| ExpressionOp::VectorProject { vector: value, .. }
|
||||
| ExpressionOp::MatrixColumn { matrix: value, .. }
|
||||
| ExpressionOp::TypeWord { value, .. }
|
||||
| ExpressionOp::U32Bit { value, .. } => vec![*value],
|
||||
ExpressionOp::AabbMin { aabb } | ExpressionOp::AabbMax { aabb } => vec![*aabb],
|
||||
ExpressionOp::FrustumCulled { local_aabb, .. } => vec![*local_aabb],
|
||||
ExpressionOp::BooleanBinary { left, right, .. }
|
||||
| ExpressionOp::CompareF32 { left, right, .. }
|
||||
| ExpressionOp::CompareU32 { left, right, .. } => vec![*left, *right],
|
||||
ExpressionOp::VectorConstruct { components } => components.clone(),
|
||||
ExpressionOp::MatrixConstruct { columns } => columns.clone(),
|
||||
ExpressionOp::TypeConstruct { words } => words.clone(),
|
||||
ExpressionOp::U32Construct { bits } => bits.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_runtime_plan(
|
||||
graph: &CompiledGraph,
|
||||
surface: RuntimeSurfaceContract,
|
||||
@@ -1095,35 +1190,13 @@ pub fn prepare_runtime_plan(
|
||||
));
|
||||
}
|
||||
let mut frame_out_index = None;
|
||||
let mut query = None;
|
||||
let mut executions = Vec::with_capacity(graph.executions.len());
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
let path = format!("executions[{i}]");
|
||||
match execution.executor.key.as_str() {
|
||||
"mesh_query" => {
|
||||
let NormalizedParameters::MeshQuery {
|
||||
visible_predicate,
|
||||
frustum_culled_predicate,
|
||||
} = &execution.parameters
|
||||
else {
|
||||
return Err(invalid("mesh query parameters mismatch", &path));
|
||||
};
|
||||
let key = MeshQueryRuntimeKey {
|
||||
visible: *visible_predicate,
|
||||
frustum_culled: *frustum_culled_predicate,
|
||||
};
|
||||
if query.replace(key).is_some() {
|
||||
return Err(error(
|
||||
"GRAPH_EXECUTION_UNSUPPORTED",
|
||||
"multiple draw stream queries",
|
||||
&path,
|
||||
));
|
||||
}
|
||||
}
|
||||
"pipeline_registry" | "pipeline" => {}
|
||||
"pipeline" => {}
|
||||
_ if contract(&execution.executor.key)
|
||||
.is_some_and(|contract| contract.fullscreen_policy.is_some()) => {}
|
||||
"frustum_cull" => {}
|
||||
"frame_out" => {
|
||||
if frame_out_index.replace(i).is_some() {
|
||||
return Err(error(
|
||||
@@ -1153,115 +1226,7 @@ pub fn prepare_runtime_plan(
|
||||
"executions",
|
||||
)
|
||||
})?;
|
||||
let query = query.ok_or_else(|| {
|
||||
error(
|
||||
"GRAPH_EXECUTION_UNSUPPORTED",
|
||||
"one mesh query is required",
|
||||
"executions",
|
||||
)
|
||||
})?;
|
||||
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
if execution.executor.key != "pipeline_registry" {
|
||||
continue;
|
||||
}
|
||||
if !matches!(execution.parameters, NormalizedParameters::PipelineRegistry) {
|
||||
return Err(invalid(
|
||||
"pipeline registry parameters mismatch",
|
||||
format!("executions[{i}].parameters"),
|
||||
));
|
||||
}
|
||||
if !matches!(execution.kind, ExecutionKind::CpuPreparation) {
|
||||
return Err(invalid(
|
||||
"pipeline registry kind mismatch",
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
}
|
||||
let [CompiledSocketInput {
|
||||
socket: input_socket,
|
||||
resource: pipeline_indices,
|
||||
}] = execution.inputs.as_slice()
|
||||
else {
|
||||
return Err(invalid(
|
||||
"pipeline registry input shape mismatch",
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
};
|
||||
if input_socket != "pipelineIndices" {
|
||||
return Err(invalid(
|
||||
"pipeline registry input socket mismatch",
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
let [CompiledSocketOutput {
|
||||
socket: output_socket,
|
||||
resource: activation,
|
||||
}] = execution.outputs.as_slice()
|
||||
else {
|
||||
return Err(invalid(
|
||||
"pipeline registry output shape mismatch",
|
||||
format!("executions[{i}].outputs"),
|
||||
));
|
||||
};
|
||||
if output_socket != "activation" {
|
||||
return Err(invalid(
|
||||
"pipeline registry output socket mismatch",
|
||||
format!("executions[{i}].outputs"),
|
||||
));
|
||||
}
|
||||
if !matches!(
|
||||
execution.accesses.as_slice(),
|
||||
[CompiledAccess { socket, resource, mode: AccessMode::SemanticRead }]
|
||||
if socket == "pipelineIndices" && resource == pipeline_indices
|
||||
) {
|
||||
return Err(invalid(
|
||||
"pipeline registry access mismatch",
|
||||
format!("executions[{i}].accesses"),
|
||||
));
|
||||
}
|
||||
let indices_resource =
|
||||
graph
|
||||
.resources
|
||||
.get(*pipeline_indices as usize)
|
||||
.ok_or_else(|| {
|
||||
invalid(
|
||||
"pipeline index stream is out of bounds",
|
||||
format!("executions[{i}].inputs"),
|
||||
)
|
||||
})?;
|
||||
let ResourcePlan::PipelineIndexStream { mesh } = indices_resource.plan else {
|
||||
return Err(invalid(
|
||||
"pipeline registry input is not a pipeline index stream",
|
||||
format!("resources[{pipeline_indices}].plan"),
|
||||
));
|
||||
};
|
||||
if indices_resource.semantic_type != SemanticType::PipelineIndexStream
|
||||
|| !graph.resources.get(mesh as usize).is_some_and(|resource| {
|
||||
resource.semantic_type == SemanticType::MeshData
|
||||
&& matches!(resource.plan, ResourcePlan::MeshData)
|
||||
})
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline index stream mesh provenance is invalid",
|
||||
format!("resources[{pipeline_indices}].plan"),
|
||||
));
|
||||
}
|
||||
let activation_resource = graph.resources.get(*activation as usize).ok_or_else(|| {
|
||||
invalid(
|
||||
"pipeline activation is out of bounds",
|
||||
format!("executions[{i}].outputs"),
|
||||
)
|
||||
})?;
|
||||
if activation_resource.semantic_type != SemanticType::PipelineActivation
|
||||
|| !matches!(activation_resource.plan, ResourcePlan::PipelineActivation { pipeline_indices: source } if source == *pipeline_indices)
|
||||
|| activation_resource.producer_execution != Some(i as u32)
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline activation provenance is invalid",
|
||||
format!("resources[{activation}].plan"),
|
||||
));
|
||||
}
|
||||
}
|
||||
validate_instance_traversal(graph)?;
|
||||
|
||||
for (i, execution) in graph.executions.iter().enumerate() {
|
||||
if execution.executor.key != "pipeline" {
|
||||
@@ -1305,9 +1270,7 @@ pub fn prepare_runtime_plan(
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
};
|
||||
let [mesh_input, draws_input, activation_input, color_input, depth_input] =
|
||||
execution.inputs.as_slice()
|
||||
else {
|
||||
let [mesh_input, color_input, depth_input] = execution.inputs.as_slice() else {
|
||||
return Err(invalid(
|
||||
"pipeline input shape mismatch",
|
||||
format!("executions[{i}].inputs"),
|
||||
@@ -1315,11 +1278,9 @@ pub fn prepare_runtime_plan(
|
||||
};
|
||||
if [
|
||||
mesh_input.socket.as_str(),
|
||||
draws_input.socket.as_str(),
|
||||
activation_input.socket.as_str(),
|
||||
color_input.socket.as_str(),
|
||||
depth_input.socket.as_str(),
|
||||
] != ["mesh", "draws", "activation", "colorTarget", "depthTarget"]
|
||||
] != ["mesh", "colorTarget", "depthTarget"]
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline input sockets mismatch",
|
||||
@@ -1394,9 +1355,7 @@ pub fn prepare_runtime_plan(
|
||||
format!("executions[{i}].kind"),
|
||||
));
|
||||
}
|
||||
let [mesh_access, draws_access, activation_access, color_access, depth_access] =
|
||||
execution.accesses.as_slice()
|
||||
else {
|
||||
let [mesh_access, color_access, depth_access] = execution.accesses.as_slice() else {
|
||||
return Err(invalid(
|
||||
"pipeline access shape mismatch",
|
||||
format!("executions[{i}].accesses"),
|
||||
@@ -1405,12 +1364,6 @@ pub fn prepare_runtime_plan(
|
||||
if mesh_access.socket != "mesh"
|
||||
|| mesh_access.resource != mesh_input.resource
|
||||
|| !matches!(mesh_access.mode, AccessMode::SemanticRead)
|
||||
|| draws_access.socket != "draws"
|
||||
|| draws_access.resource != draws_input.resource
|
||||
|| !matches!(draws_access.mode, AccessMode::IndirectRead)
|
||||
|| activation_access.socket != "activation"
|
||||
|| activation_access.resource != activation_input.resource
|
||||
|| !matches!(activation_access.mode, AccessMode::SemanticRead)
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline semantic accesses mismatch",
|
||||
@@ -1459,73 +1412,6 @@ pub fn prepare_runtime_plan(
|
||||
format!("resources[{}].plan", mesh_input.resource),
|
||||
));
|
||||
}
|
||||
let draws_mesh = match graph.resources.get(draws_input.resource as usize) {
|
||||
Some(CompiledResource {
|
||||
semantic_type: SemanticType::DrawStream,
|
||||
plan: ResourcePlan::DrawStream { mesh },
|
||||
..
|
||||
}) => *mesh,
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline draw stream is invalid",
|
||||
format!("resources[{}].plan", draws_input.resource),
|
||||
))
|
||||
}
|
||||
};
|
||||
let activation_resource = graph
|
||||
.resources
|
||||
.get(activation_input.resource as usize)
|
||||
.ok_or_else(|| {
|
||||
invalid(
|
||||
"pipeline activation is out of bounds",
|
||||
format!("executions[{i}].inputs"),
|
||||
)
|
||||
})?;
|
||||
let indices = match activation_resource.plan {
|
||||
ResourcePlan::PipelineActivation { pipeline_indices }
|
||||
if activation_resource.semantic_type == SemanticType::PipelineActivation =>
|
||||
{
|
||||
pipeline_indices
|
||||
}
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline activation is invalid",
|
||||
format!("resources[{}].plan", activation_input.resource),
|
||||
))
|
||||
}
|
||||
};
|
||||
let activation_mesh = match graph.resources.get(indices as usize) {
|
||||
Some(CompiledResource {
|
||||
semantic_type: SemanticType::PipelineIndexStream,
|
||||
plan: ResourcePlan::PipelineIndexStream { mesh },
|
||||
..
|
||||
}) => *mesh,
|
||||
_ => {
|
||||
return Err(invalid(
|
||||
"pipeline activation index stream is invalid",
|
||||
format!("resources[{indices}].plan"),
|
||||
))
|
||||
}
|
||||
};
|
||||
let valid_activation_producer = activation_resource
|
||||
.producer_execution
|
||||
.and_then(|producer| graph.executions.get(producer as usize))
|
||||
.is_some_and(|producer| producer.executor.key == "pipeline_registry");
|
||||
if draws_mesh != mesh_input.resource
|
||||
|| activation_mesh != mesh_input.resource
|
||||
|| !valid_activation_producer
|
||||
{
|
||||
return Err(invalid(
|
||||
"pipeline mesh provenance disagrees",
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
if !has_exact_producer(graph, i, draws_input.resource, "mesh_query", "draws") {
|
||||
return Err(invalid(
|
||||
"pipeline draw stream producer mismatch",
|
||||
format!("executions[{i}].inputs"),
|
||||
));
|
||||
}
|
||||
for (output, target) in [
|
||||
(color_output.resource, color_input.resource),
|
||||
(depth_output.resource, depth_input.resource),
|
||||
@@ -1969,9 +1855,9 @@ pub fn prepare_runtime_plan(
|
||||
allocations: RuntimeAllocationPlan {
|
||||
classes,
|
||||
resource_allocations,
|
||||
query,
|
||||
},
|
||||
executions,
|
||||
instance_traversal: graph.instance_traversal.clone(),
|
||||
surface,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,41 +106,6 @@ pub struct TextureDescriptor {
|
||||
pub view_formats: Vec<TextureFormat>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TriStatePredicate {
|
||||
Any,
|
||||
RequiredTrue,
|
||||
RequiredFalse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RuntimePredicate {
|
||||
Any,
|
||||
RequiredTrue,
|
||||
RequiredFalse,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum MeshFlag {
|
||||
IsVisible,
|
||||
IsFrustumCulled,
|
||||
}
|
||||
|
||||
impl MeshFlag {
|
||||
pub const ORDERED: [Self; 2] = [Self::IsVisible, Self::IsFrustumCulled];
|
||||
|
||||
pub const fn input_socket(self) -> &'static str {
|
||||
match self {
|
||||
Self::IsVisible => "isVisible",
|
||||
Self::IsFrustumCulled => "isFrustumCulled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompareFunction {
|
||||
|
||||
+129
-3571
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
||||
struct Params { planes: array<vec4<f32>, 6>, count: u32, visible_predicate: u32, frustum_predicate: u32, _pad: u32 }
|
||||
struct Instance { model: mat4x4<f32>, n0: vec4<f32>, n1: vec4<f32>, n2: vec4<f32> }
|
||||
struct Aabb { min: vec4<f32>, max: vec4<f32> }
|
||||
struct Meta { index_count: u32, first_index: u32, base_vertex: i32, instance_index: u32 }
|
||||
struct Command { index_count: u32, instance_count: u32, first_index: u32, base_vertex: i32, first_instance: u32 }
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read> instances: array<Instance>;
|
||||
@group(0) @binding(2) var<storage, read> bounds: array<Aabb>;
|
||||
@group(0) @binding(3) var<storage, read> authored_visible: array<u32>;
|
||||
@group(0) @binding(4) var<storage, read> metadata: array<Meta>;
|
||||
@group(0) @binding(5) var<storage, read_write> frustum_flags: array<u32>;
|
||||
@group(0) @binding(6) var<storage, read_write> commands: array<Command>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn frustum_cull(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let i = id.x;
|
||||
if (i >= params.count) { return; }
|
||||
let b = bounds[i];
|
||||
let center = (b.min.xyz + b.max.xyz) * 0.5;
|
||||
let extent = (b.max.xyz - b.min.xyz) * 0.5;
|
||||
let m = instances[i].model;
|
||||
let wc = (m * vec4<f32>(center, 1.0)).xyz;
|
||||
let ax = m[0].xyz * extent.x;
|
||||
let ay = m[1].xyz * extent.y;
|
||||
let az = m[2].xyz * extent.z;
|
||||
var inside = 1u;
|
||||
for (var p = 0u; p < 6u; p++) {
|
||||
let plane = params.planes[p];
|
||||
let radius = abs(dot(plane.xyz, ax)) + abs(dot(plane.xyz, ay)) + abs(dot(plane.xyz, az));
|
||||
if (dot(plane.xyz, wc) + plane.w + radius < 0.0) { inside = 0u; }
|
||||
}
|
||||
frustum_flags[i] = 1u - inside;
|
||||
}
|
||||
|
||||
fn matches(value: u32, predicate: u32) -> bool {
|
||||
return predicate == 0u || (predicate == 1u && value != 0u) || (predicate == 2u && value == 0u);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn mesh_query(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let i = id.x;
|
||||
if (i >= params.count) { return; }
|
||||
let draw_meta = metadata[i];
|
||||
var selected = true;
|
||||
if (params.visible_predicate == 3u) {
|
||||
selected = false;
|
||||
} else if (params.visible_predicate != 0u) {
|
||||
selected = matches(authored_visible[i], params.visible_predicate);
|
||||
}
|
||||
if (selected && params.frustum_predicate == 3u) {
|
||||
selected = false;
|
||||
} else if (selected && params.frustum_predicate != 0u) {
|
||||
selected = selected && matches(frustum_flags[i], params.frustum_predicate);
|
||||
}
|
||||
commands[i] = Command(draw_meta.index_count, select(0u, 1u, selected), draw_meta.first_index, draw_meta.base_vertex, 0u);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ fn encode_scene<'a, T: Scene>(
|
||||
pass.set_vertex_buffer(4, t.slice(..));
|
||||
pass.set_index_buffer(i.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for draw in &gpu.draws {
|
||||
if !draw.effective_visible {
|
||||
if !draw.instance_type.is_visible() {
|
||||
continue;
|
||||
}
|
||||
pass.set_pipeline(pipelines.get_pipeline(draw.pipeline));
|
||||
@@ -54,6 +54,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
gpu: &GpuSceneCache,
|
||||
pipelines: &PipelineLibrary,
|
||||
materials: &MaterialResources,
|
||||
indirect_commands: &wgpu::Buffer,
|
||||
mut profile: Option<&mut crate::renderer::profiler::ProfileFrame>,
|
||||
) -> Result<(), &'static str> {
|
||||
use crate::render_graph::{ExecutionKind, NormalizedColorLoad, NormalizedDepthLoad, StoreOp};
|
||||
@@ -73,16 +74,8 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
.map(|s| &s.view)
|
||||
.ok_or(" allocation out of bounds")
|
||||
};
|
||||
for (execution_index, prepared) in active.executions.iter().enumerate() {
|
||||
let profile_id = &active.graph.executions[execution_index].id;
|
||||
for prepared in &active.executions {
|
||||
match prepared {
|
||||
PreparedExecution::PipelineRegistry => {}
|
||||
PreparedExecution::FrustumCull => {
|
||||
gpu.encode_frustum_cull(encoder, profile.as_deref_mut(), profile_id);
|
||||
}
|
||||
PreparedExecution::MeshQuery => {
|
||||
gpu.encode_mesh_query(encoder, profile.as_deref_mut(), profile_id);
|
||||
}
|
||||
PreparedExecution::Fullscreen {
|
||||
execution,
|
||||
frame_out,
|
||||
@@ -159,6 +152,7 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
PreparedExecution::Pipeline {
|
||||
execution,
|
||||
base,
|
||||
predicate_ordinal,
|
||||
variant,
|
||||
} => {
|
||||
let execution = active
|
||||
@@ -236,24 +230,19 @@ pub(crate) fn encode_compiled<T: Scene>(
|
||||
pass.set_vertex_buffer(2, u.slice(..));
|
||||
pass.set_vertex_buffer(4, t.slice(..));
|
||||
pass.set_index_buffer(ix.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for draw in &gpu.draws {
|
||||
if draw.pipeline != *base {
|
||||
continue;
|
||||
}
|
||||
let slot = draw.instances.start as u64;
|
||||
let start = slot
|
||||
* std::mem::size_of::<crate::renderer::gpu_scene::GpuInstance>() as u64;
|
||||
pass.set_vertex_buffer(3, inst.slice(start..start + 112));
|
||||
pass.set_vertex_buffer(3, inst.slice(..));
|
||||
for (draw_index, draw) in gpu.draws.iter().enumerate() {
|
||||
pass.set_pipeline(variant);
|
||||
if pipelines.requires_material(draw.pipeline) {
|
||||
if pipelines.requires_material(*base) {
|
||||
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
||||
}
|
||||
pass.draw_indexed_indirect(
|
||||
gpu.indirect_commands
|
||||
.buffer
|
||||
.as_ref()
|
||||
.ok_or("indirect command buffer missing")?,
|
||||
slot * 20,
|
||||
indirect_commands,
|
||||
crate::renderer::instance_traversal::command_offset(
|
||||
*predicate_ordinal,
|
||||
gpu.draws.len(),
|
||||
draw_index,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+243
-663
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
//! Graph-owned instance predicate compute support.
|
||||
|
||||
use crate::render_graph::{
|
||||
BooleanBinaryOp, CompareOp, ExpressionOp, InstanceTraversalPlan, SemanticType, TypedLiteral,
|
||||
};
|
||||
|
||||
use super::gpu_scene::{DrawIndexedIndirect, GpuSceneCache};
|
||||
|
||||
fn f(value: f32) -> Result<String, String> {
|
||||
if !value.is_finite() {
|
||||
return Err("non-finite expression literal".into());
|
||||
}
|
||||
Ok(format!("{value:?}"))
|
||||
}
|
||||
|
||||
/// Generates the dense, single-invocation traversal body. Expressions are emitted in
|
||||
/// `ExprId` order, so shared IR nodes are evaluated exactly once.
|
||||
pub fn generate_wgsl(plan: &InstanceTraversalPlan) -> Result<String, String> {
|
||||
let mut s = String::from("struct Params { planes: array<vec4<f32>,6>, instance_count:u32, pipeline_count:u32, _pad:vec2<u32> };\nstruct Inst{model:mat4x4<f32>,n0:vec4<f32>,n1:vec4<f32>,n2:vec4<f32>}; struct Aabb{min:vec4<f32>,max:vec4<f32>}; struct Type16{words:array<u32,16>}; struct Meta{index_count:u32,first_index:u32,base_vertex:i32,instance_index:u32}; struct Cmd{index_count:u32,instance_count:u32,first_index:u32,base_vertex:i32,first_instance:u32};\n@group(0) @binding(0)var<uniform>p:Params; @group(0) @binding(1)var<storage,read>instances:array<Inst>; @group(0) @binding(2)var<storage,read>aabbs:array<Aabb>; @group(0) @binding(3)var<storage,read>types:array<Type16>; @group(0) @binding(4)var<storage,read>metadata:array<Meta>; @group(0) @binding(5)var<storage,read_write>commands:array<Cmd>;\nstruct LocalAabb{min:vec3<f32>,max:vec3<f32>}; fn culled(i:u32,a:LocalAabb)->bool{var outside=false;for(var q=0u;q<6u;q++){var all=true;for(var c=0u;c<8u;c++){let v=vec3<f32>(select(a.min.x,a.max.x,(c&1u)!=0u),select(a.min.y,a.max.y,(c&2u)!=0u),select(a.min.z,a.max.z,(c&4u)!=0u));all=all&&(dot(p.planes[q],instances[i].model*vec4<f32>(v,1.0))<0.0);}outside=outside||all;}return outside;} @compute @workgroup_size(64) fn main(@builtin(global_invocation_id)gid:vec3<u32>){let i=gid.x;if(i>=p.instance_count){return;}\n");
|
||||
for (i, e) in plan.expressions.expressions.iter().enumerate() {
|
||||
let x = |id: crate::render_graph::ExprId| format!("e{}", id.0);
|
||||
let rhs = match &e.op {
|
||||
ExpressionOp::Literal { literal } => match literal {
|
||||
TypedLiteral::Bool(v) => v.to_string(),
|
||||
TypedLiteral::F32(v) => f(*v)?,
|
||||
TypedLiteral::U32(v) => format!("{v}u"),
|
||||
TypedLiteral::Vec2(v) => format!("vec2<f32>({},{})", f(v[0])?, f(v[1])?),
|
||||
TypedLiteral::Vec3(v) => {
|
||||
format!("vec3<f32>({},{},{})", f(v[0])?, f(v[1])?, f(v[2])?)
|
||||
}
|
||||
TypedLiteral::Vec4(v) => format!(
|
||||
"vec4<f32>({},{},{},{})",
|
||||
f(v[0])?,
|
||||
f(v[1])?,
|
||||
f(v[2])?,
|
||||
f(v[3])?
|
||||
),
|
||||
TypedLiteral::U32x16(v) => format!(
|
||||
"Type16(array<u32,16>({}))",
|
||||
v.iter()
|
||||
.map(|x| format!("{x}u"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
),
|
||||
TypedLiteral::LocalAabb { min, max } => format!(
|
||||
"LocalAabb(vec3<f32>({},{},{}),vec3<f32>({},{},{}))",
|
||||
f(min[0])?,
|
||||
f(min[1])?,
|
||||
f(min[2])?,
|
||||
f(max[0])?,
|
||||
f(max[1])?,
|
||||
f(max[2])?
|
||||
),
|
||||
TypedLiteral::Mat2(v) => matrix_literal("mat2x2<f32>", v)?,
|
||||
TypedLiteral::Mat3(v) => matrix_literal("mat3x3<f32>", v)?,
|
||||
TypedLiteral::Mat4(v) => matrix_literal("mat4x4<f32>", v)?,
|
||||
},
|
||||
ExpressionOp::InstanceType { .. } => "types[i]".into(),
|
||||
ExpressionOp::LocalAabb { .. } => "LocalAabb(aabbs[i].min.xyz,aabbs[i].max.xyz)".into(),
|
||||
ExpressionOp::Not { value } => format!("!{}", x(*value)),
|
||||
ExpressionOp::BooleanBinary {
|
||||
operation,
|
||||
left,
|
||||
right,
|
||||
} => format!(
|
||||
"({} {} {})",
|
||||
x(*left),
|
||||
match operation {
|
||||
BooleanBinaryOp::And => "&&",
|
||||
BooleanBinaryOp::Or => "||",
|
||||
BooleanBinaryOp::Xor => "!=",
|
||||
BooleanBinaryOp::Xnor => "==",
|
||||
},
|
||||
x(*right)
|
||||
),
|
||||
ExpressionOp::CompareF32 {
|
||||
operation,
|
||||
left,
|
||||
right,
|
||||
}
|
||||
| ExpressionOp::CompareU32 {
|
||||
operation,
|
||||
left,
|
||||
right,
|
||||
} => format!(
|
||||
"({} {} {})",
|
||||
x(*left),
|
||||
match operation {
|
||||
CompareOp::GreaterThan => ">",
|
||||
CompareOp::LessThan => "<",
|
||||
CompareOp::Equals => "==",
|
||||
},
|
||||
x(*right)
|
||||
),
|
||||
ExpressionOp::VectorProject { vector, index } => {
|
||||
let limit =
|
||||
vector_width(&plan.expressions.expressions[vector.0 as usize].semantic_type)
|
||||
.ok_or("vector projection source is not a vector")?;
|
||||
fixed_index(*index, limit, "vector projection")?;
|
||||
format!("{}[{}]", x(*vector), index)
|
||||
}
|
||||
ExpressionOp::VectorConstruct { components } => format!(
|
||||
"{}({})",
|
||||
wgsl_type(&e.semantic_type)?,
|
||||
components
|
||||
.iter()
|
||||
.map(|id| x(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
),
|
||||
ExpressionOp::MatrixColumn { matrix, index } => {
|
||||
let limit =
|
||||
matrix_width(&plan.expressions.expressions[matrix.0 as usize].semantic_type)
|
||||
.ok_or("matrix projection source is not a matrix")?;
|
||||
fixed_index(*index, limit, "matrix projection")?;
|
||||
format!("{}[{}]", x(*matrix), index)
|
||||
}
|
||||
ExpressionOp::MatrixConstruct { columns } => format!(
|
||||
"{}({})",
|
||||
wgsl_type(&e.semantic_type)?,
|
||||
columns
|
||||
.iter()
|
||||
.map(|id| x(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
),
|
||||
ExpressionOp::TypeWord { value, index } => {
|
||||
fixed_index(*index, 16, "u32x16 projection")?;
|
||||
format!("{}.words[{}]", x(*value), index)
|
||||
}
|
||||
ExpressionOp::TypeConstruct { words } => format!(
|
||||
"Type16(array<u32,16>({}))",
|
||||
words.iter().map(|id| x(*id)).collect::<Vec<_>>().join(",")
|
||||
),
|
||||
ExpressionOp::U32Bit { value, index } => {
|
||||
fixed_index(*index, 32, "u32 bit projection")?;
|
||||
format!("(({} & (1u<<{}u))!=0u)", x(*value), index)
|
||||
}
|
||||
ExpressionOp::U32Construct { bits } => bits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(bit, id)| format!("select(0u,{}u,{})", 1u32 << bit, x(*id)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
.pipe(|terms| format!("({terms})")),
|
||||
ExpressionOp::AabbMin { aabb } => format!("{}.min", x(*aabb)),
|
||||
ExpressionOp::AabbMax { aabb } => format!("{}.max", x(*aabb)),
|
||||
ExpressionOp::FrustumCulled { local_aabb, .. } => {
|
||||
format!("culled(i,{})", x(*local_aabb))
|
||||
}
|
||||
};
|
||||
s.push_str(&format!("let e{i}={rhs};\n"));
|
||||
}
|
||||
for entry in &plan.pipelines {
|
||||
s.push_str(&format!("{{let m=metadata[i];commands[{}u*p.instance_count+i]=Cmd(m.index_count,select(0u,1u,e{}),m.first_index,m.base_vertex,m.instance_index);}}\n",entry.ordinal,entry.predicate.0));
|
||||
}
|
||||
s.push('}');
|
||||
if s.len() >= 1024 * 1024 {
|
||||
return Err("generated traversal WGSL exceeds 1 MiB".into());
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn fixed_index(index: u8, limit: u8, kind: &str) -> Result<(), String> {
|
||||
(index < limit)
|
||||
.then_some(())
|
||||
.ok_or_else(|| format!("invalid fixed {kind} index"))
|
||||
}
|
||||
fn vector_width(ty: &SemanticType) -> Option<u8> {
|
||||
match ty {
|
||||
SemanticType::Vec2 => Some(2),
|
||||
SemanticType::Vec3 => Some(3),
|
||||
SemanticType::Vec4 => Some(4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn matrix_width(ty: &SemanticType) -> Option<u8> {
|
||||
match ty {
|
||||
SemanticType::Mat2 => Some(2),
|
||||
SemanticType::Mat3 => Some(3),
|
||||
SemanticType::Mat4 => Some(4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn wgsl_type(ty: &SemanticType) -> Result<&'static str, String> {
|
||||
match ty {
|
||||
SemanticType::Vec2 => Ok("vec2<f32>"),
|
||||
SemanticType::Vec3 => Ok("vec3<f32>"),
|
||||
SemanticType::Vec4 => Ok("vec4<f32>"),
|
||||
SemanticType::Mat2 => Ok("mat2x2<f32>"),
|
||||
SemanticType::Mat3 => Ok("mat3x3<f32>"),
|
||||
SemanticType::Mat4 => Ok("mat4x4<f32>"),
|
||||
_ => Err("invalid combine result type".into()),
|
||||
}
|
||||
}
|
||||
fn matrix_literal<const N: usize>(name: &str, columns: &[[f32; N]; N]) -> Result<String, String> {
|
||||
let values = columns
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|v| f(*v))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(format!("{name}({})", values.join(",")))
|
||||
}
|
||||
trait Pipe: Sized {
|
||||
fn pipe<R>(self, f: impl FnOnce(Self) -> R) -> R {
|
||||
f(self)
|
||||
}
|
||||
}
|
||||
impl<T> Pipe for T {}
|
||||
|
||||
pub fn dispatch_count(instances: u32, pipelines: u32) -> u32 {
|
||||
if pipelines == 0 {
|
||||
0
|
||||
} else {
|
||||
instances.max(1).div_ceil(64)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_offset(predicate_ordinal: u32, instance_count: usize, draw_index: usize) -> u64 {
|
||||
(u64::from(predicate_ordinal) * instance_count as u64 + draw_index as u64)
|
||||
* std::mem::size_of::<DrawIndexedIndirect>() as u64
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Params {
|
||||
planes: [[f32; 4]; 6],
|
||||
instance_count: u32,
|
||||
pipeline_count: u32,
|
||||
pad: [u32; 2],
|
||||
}
|
||||
|
||||
pub struct TraversalGpu {
|
||||
graph: crate::render_graph::CompiledGraphId,
|
||||
plan: InstanceTraversalPlan,
|
||||
scene_epoch: u64,
|
||||
draw_count: usize,
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
params: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
pub commands: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl TraversalGpu {
|
||||
pub fn matches(
|
||||
&self,
|
||||
graph: crate::render_graph::CompiledGraphId,
|
||||
plan: &InstanceTraversalPlan,
|
||||
scene_epoch: u64,
|
||||
draw_count: usize,
|
||||
) -> bool {
|
||||
self.graph == graph
|
||||
&& self.plan == *plan
|
||||
&& self.scene_epoch == scene_epoch
|
||||
&& self.draw_count == draw_count
|
||||
}
|
||||
pub fn create(
|
||||
device: &wgpu::Device,
|
||||
graph: crate::render_graph::CompiledGraphId,
|
||||
plan: &InstanceTraversalPlan,
|
||||
gpu: &GpuSceneCache,
|
||||
) -> Result<Self, String> {
|
||||
let source = generate_wgsl(plan)?;
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("instance traversal"),
|
||||
source: wgpu::ShaderSource::Wgsl(source.into()),
|
||||
});
|
||||
let entries = [
|
||||
(0, wgpu::BufferBindingType::Uniform),
|
||||
(1, wgpu::BufferBindingType::Storage { read_only: true }),
|
||||
(2, wgpu::BufferBindingType::Storage { read_only: true }),
|
||||
(3, wgpu::BufferBindingType::Storage { read_only: true }),
|
||||
(4, wgpu::BufferBindingType::Storage { read_only: true }),
|
||||
(5, wgpu::BufferBindingType::Storage { read_only: false }),
|
||||
]
|
||||
.map(|(binding, ty)| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
});
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("instance traversal"),
|
||||
entries: &entries,
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("instance traversal"),
|
||||
bind_group_layouts: &[&layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("instance traversal"),
|
||||
layout: Some(&pipeline_layout),
|
||||
module: &module,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
let params = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("instance traversal params"),
|
||||
size: std::mem::size_of::<Params>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let count = (gpu.draws.len() as u64)
|
||||
.checked_mul(plan.pipelines.len() as u64)
|
||||
.and_then(|n| n.checked_mul(std::mem::size_of::<DrawIndexedIndirect>() as u64))
|
||||
.ok_or("indirect command size overflow")?
|
||||
.max(std::mem::size_of::<DrawIndexedIndirect>() as u64);
|
||||
if count > device.limits().max_buffer_size {
|
||||
return Err("indirect command buffer exceeds device limit".into());
|
||||
}
|
||||
let commands = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("instance traversal commands"),
|
||||
size: count,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::INDIRECT,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
fn required(slot: &super::gpu_scene::BufferSlot) -> Result<&wgpu::Buffer, String> {
|
||||
slot.buffer
|
||||
.as_ref()
|
||||
.ok_or_else(|| "instance traversal scene buffer missing".to_owned())
|
||||
}
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("instance traversal"),
|
||||
layout: &layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: params.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: required(&gpu.instances)?.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: required(&gpu.local_aabbs)?.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: required(&gpu.instance_types)?.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: required(&gpu.draw_metadata)?.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: commands.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
Ok(Self {
|
||||
graph,
|
||||
plan: plan.clone(),
|
||||
scene_epoch: gpu.buffer_epoch,
|
||||
draw_count: gpu.draws.len(),
|
||||
pipeline,
|
||||
params,
|
||||
bind_group,
|
||||
commands,
|
||||
})
|
||||
}
|
||||
pub(crate) fn encode(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
queue: &wgpu::Queue,
|
||||
planes: Option<[[f32; 4]; 6]>,
|
||||
instances: u32,
|
||||
mut profile: Option<&mut super::profiler::ProfileFrame>,
|
||||
) {
|
||||
queue.write_buffer(
|
||||
&self.params,
|
||||
0,
|
||||
bytemuck::bytes_of(&Params {
|
||||
planes: planes.unwrap_or([[0.; 4]; 6]),
|
||||
instance_count: instances,
|
||||
pipeline_count: self.plan.pipelines.len() as u32,
|
||||
pad: [0; 2],
|
||||
}),
|
||||
);
|
||||
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some("instance traversal"),
|
||||
timestamp_writes: profile
|
||||
.as_deref_mut()
|
||||
.and_then(|p| p.compute_writes("instance_traversal")),
|
||||
});
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||
pass.dispatch_workgroups(
|
||||
dispatch_count(instances, self.plan.pipelines.len() as u32),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pipeline_major_offsets_and_single_dispatch_are_deterministic() {
|
||||
assert_eq!(command_offset(0, 7, 6), 120);
|
||||
assert_eq!(command_offset(1, 7, 0), 140);
|
||||
assert_eq!(command_offset(3, 7, 2), 460);
|
||||
assert_eq!(dispatch_count(1, 4), 1);
|
||||
assert_eq!(dispatch_count(64, 4), 1);
|
||||
assert_eq!(dispatch_count(65, 4), 2);
|
||||
assert_eq!(dispatch_count(0, 4), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lowering_helpers_cover_matrices_and_reject_dynamic_indexes() {
|
||||
assert_eq!(
|
||||
matrix_literal("mat2x2<f32>", &[[1.0, 2.0], [3.0, 4.0]]).unwrap(),
|
||||
"mat2x2<f32>(1.0,2.0,3.0,4.0)"
|
||||
);
|
||||
assert!(fixed_index(3, 3, "vector projection").is_err());
|
||||
assert_eq!(wgsl_type(&SemanticType::Mat4).unwrap(), "mat4x4<f32>");
|
||||
}
|
||||
}
|
||||
+221
-113
@@ -11,12 +11,13 @@ use crate::{
|
||||
command_ring::CommandRing,
|
||||
gltf::{install_imported, ModelBounds},
|
||||
message::{camera_drag, CameraDrag, DrainEventError, MouseMessage, ResizeMessage, WindowEvent},
|
||||
render_data::{InstanceHandle, MeshHandle, RenderData, RenderDataConfig, RenderFlags},
|
||||
render_data::{InstanceHandle, InstanceType, MeshHandle, RenderData, RenderDataConfig},
|
||||
renderer::scene::Scene,
|
||||
};
|
||||
|
||||
pub mod executors;
|
||||
pub mod gpu_scene;
|
||||
pub mod instance_traversal;
|
||||
pub mod material;
|
||||
pub mod pipeline_library;
|
||||
pub mod profiler;
|
||||
@@ -27,6 +28,43 @@ pub use pipeline_library::PipelineLibrary;
|
||||
|
||||
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct DeviceFeaturePlan {
|
||||
hard: wgpu::Features,
|
||||
initial: wgpu::Features,
|
||||
profiling_enabled: bool,
|
||||
}
|
||||
|
||||
fn device_feature_plan(profile: bool, supported: wgpu::Features) -> DeviceFeaturePlan {
|
||||
let hard = wgpu::Features::INDIRECT_FIRST_INSTANCE;
|
||||
let profiling = profiler::Profiler::requested_features(profile, supported);
|
||||
DeviceFeaturePlan {
|
||||
hard,
|
||||
initial: hard | profiling,
|
||||
profiling_enabled: !profiling.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod device_feature_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retry_plan_never_drops_hard_features() {
|
||||
let hard_only = device_feature_plan(false, wgpu::Features::INDIRECT_FIRST_INSTANCE);
|
||||
assert_eq!(hard_only.initial, hard_only.hard);
|
||||
assert!(!hard_only.profiling_enabled);
|
||||
|
||||
let profiled = device_feature_plan(
|
||||
true,
|
||||
wgpu::Features::INDIRECT_FIRST_INSTANCE | wgpu::Features::TIMESTAMP_QUERY,
|
||||
);
|
||||
assert!(profiled.initial.contains(profiled.hard));
|
||||
assert!(profiled.initial.contains(wgpu::Features::TIMESTAMP_QUERY));
|
||||
assert!(profiled.profiling_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, align(16))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct FullscreenUniforms {
|
||||
@@ -748,12 +786,10 @@ struct GpuTextureSlot {
|
||||
}
|
||||
|
||||
enum PreparedExecution {
|
||||
FrustumCull,
|
||||
MeshQuery,
|
||||
PipelineRegistry,
|
||||
Pipeline {
|
||||
execution: usize,
|
||||
base: crate::render_data::PipelineKey,
|
||||
predicate_ordinal: u32,
|
||||
variant: wgpu::RenderPipeline,
|
||||
},
|
||||
Fullscreen {
|
||||
@@ -777,7 +813,7 @@ struct ActiveCompiledGraph {
|
||||
#[derive(Clone, Copy)]
|
||||
enum UploadGraph {
|
||||
Immediate,
|
||||
Compiled(crate::render_graph::MeshQueryRuntimeKey),
|
||||
Compiled(bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -832,27 +868,27 @@ fn acquisition_action(source: FrameTargetSource, error: &wgpu::SurfaceError) ->
|
||||
}
|
||||
|
||||
fn classify_upload_graph(graph: &ActiveCompiledGraph) -> UploadGraph {
|
||||
UploadGraph::Compiled(graph.runtime.allocations.query)
|
||||
UploadGraph::Compiled(
|
||||
graph
|
||||
.runtime
|
||||
.instance_traversal
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.requires_camera),
|
||||
)
|
||||
}
|
||||
|
||||
fn upload_query_for_render(
|
||||
pending: Option<UploadGraph>,
|
||||
active: Option<UploadGraph>,
|
||||
) -> Option<crate::render_graph::MeshQueryRuntimeKey> {
|
||||
fn upload_query_for_render(pending: Option<UploadGraph>, active: Option<UploadGraph>) -> bool {
|
||||
match pending.or(active) {
|
||||
Some(UploadGraph::Compiled(query)) => Some(query),
|
||||
Some(UploadGraph::Immediate) | None => None,
|
||||
Some(UploadGraph::Compiled(value)) => value,
|
||||
Some(UploadGraph::Immediate) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_culling_frustum(
|
||||
query: crate::render_graph::MeshQueryRuntimeKey,
|
||||
required: bool,
|
||||
read: impl FnOnce() -> Option<Result<[[f32; 4]; 6], crate::camera::FrustumError>>,
|
||||
) -> Result<Option<[[f32; 4]; 6]>, crate::render_graph::GraphError> {
|
||||
if matches!(
|
||||
query.frustum_culled,
|
||||
crate::render_graph::RuntimePredicate::Any | crate::render_graph::RuntimePredicate::Never
|
||||
) {
|
||||
if !required {
|
||||
return Ok(None);
|
||||
}
|
||||
match read() {
|
||||
@@ -871,13 +907,10 @@ fn resolve_culling_frustum(
|
||||
fn update_validate_write_scene<S: scene::Scene>(
|
||||
scene: &mut S,
|
||||
queue: &wgpu::Queue,
|
||||
query: Option<crate::render_graph::MeshQueryRuntimeKey>,
|
||||
requires_camera: bool,
|
||||
) -> Result<Option<[[f32; 4]; 6]>, crate::render_graph::GraphError> {
|
||||
scene.update_cpu();
|
||||
let planes = match query {
|
||||
Some(query) => resolve_culling_frustum(query, || scene.frustum_planes())?,
|
||||
None => None,
|
||||
};
|
||||
let planes = resolve_culling_frustum(requires_camera, || scene.frustum_planes())?;
|
||||
scene.write_uniforms(queue);
|
||||
Ok(planes)
|
||||
}
|
||||
@@ -935,7 +968,7 @@ fn resolve_switch_request(
|
||||
1 => {
|
||||
let id = crate::render_graph::CompiledGraphId { slot, generation };
|
||||
// Resolve the registry entry here, before any GPU preparation or pending
|
||||
// state mutation. Registry::get is also the Phase 4 activation gate.
|
||||
// state mutation. Registry::get is also the compiled-graph availability gate.
|
||||
registry.get(id)?;
|
||||
Ok(ResolvedSwitchRequest::Compiled(id))
|
||||
}
|
||||
@@ -980,42 +1013,29 @@ mod switch_request_tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn query(visible: crate::render_graph::RuntimePredicate) -> UploadGraph {
|
||||
UploadGraph::Compiled(crate::render_graph::MeshQueryRuntimeKey {
|
||||
visible,
|
||||
frustum_culled: crate::render_graph::RuntimePredicate::Any,
|
||||
})
|
||||
fn graph(requires_camera: bool) -> UploadGraph {
|
||||
UploadGraph::Compiled(requires_camera)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_selection_follows_the_graph_rendered_for_the_commit_frame() {
|
||||
use crate::render_graph::RuntimePredicate::{Any, RequiredFalse, RequiredTrue};
|
||||
let selected =
|
||||
|pending, active| upload_query_for_render(pending, active).map(|query| query.visible);
|
||||
let selected = upload_query_for_render;
|
||||
assert!(!selected(Some(graph(false)), Some(graph(true))));
|
||||
assert_eq!(
|
||||
selected(Some(query(RequiredFalse)), Some(query(RequiredTrue))),
|
||||
Some(RequiredFalse)
|
||||
selected(Some(UploadGraph::Immediate), Some(graph(true))),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
selected(Some(UploadGraph::Immediate), Some(query(RequiredTrue))),
|
||||
None
|
||||
selected(Some(UploadGraph::Immediate), Some(graph(true))),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
selected(Some(UploadGraph::Immediate), Some(query(RequiredTrue))),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
selected(None, Some(query(RequiredTrue))),
|
||||
Some(RequiredTrue)
|
||||
);
|
||||
assert_eq!(selected(None, Some(UploadGraph::Immediate)), None);
|
||||
assert_eq!(selected(None, None), None);
|
||||
assert_eq!(selected(Some(query(Any)), None), Some(Any));
|
||||
assert!(selected(None, Some(graph(true))));
|
||||
assert!(!selected(None, Some(UploadGraph::Immediate)));
|
||||
assert!(!selected(None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_target_precedence_and_pending_resize_upload_are_exact() {
|
||||
use crate::render_graph::RuntimePredicate::{RequiredFalse, RequiredTrue};
|
||||
assert_eq!(
|
||||
select_frame_target_source(true, true, true),
|
||||
FrameTargetSource::PendingSwitch
|
||||
@@ -1032,14 +1052,10 @@ mod switch_request_tests {
|
||||
select_frame_target_source(false, false, false),
|
||||
FrameTargetSource::Immediate
|
||||
);
|
||||
let pending_resize = query(RequiredFalse);
|
||||
let active = query(RequiredTrue);
|
||||
assert_eq!(
|
||||
upload_query_for_render(Some(pending_resize), Some(active))
|
||||
.unwrap()
|
||||
.visible,
|
||||
RequiredFalse
|
||||
);
|
||||
assert!(!upload_query_for_render(
|
||||
Some(graph(false)),
|
||||
Some(graph(true))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1063,15 +1079,10 @@ mod switch_request_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frustum_preflight_skips_any_and_distinguishes_missing_from_invalid() {
|
||||
use crate::render_graph::RuntimePredicate::{Any, RequiredFalse, RequiredTrue};
|
||||
let query = |frustum_culled| crate::render_graph::MeshQueryRuntimeKey {
|
||||
visible: RequiredTrue,
|
||||
frustum_culled,
|
||||
};
|
||||
fn frustum_preflight_uses_boolean_traversal_requirement() {
|
||||
let mut reads = 0;
|
||||
assert_eq!(
|
||||
resolve_culling_frustum(query(Any), || {
|
||||
resolve_culling_frustum(false, || {
|
||||
reads += 1;
|
||||
None
|
||||
})
|
||||
@@ -1082,9 +1093,9 @@ mod switch_request_tests {
|
||||
reads, 0,
|
||||
"inactive frustum filtering must not read the camera"
|
||||
);
|
||||
let missing = resolve_culling_frustum(query(RequiredFalse), || None).unwrap_err();
|
||||
let missing = resolve_culling_frustum(true, || None).unwrap_err();
|
||||
assert!(missing.message.contains("no camera"));
|
||||
let invalid = resolve_culling_frustum(query(RequiredFalse), || {
|
||||
let invalid = resolve_culling_frustum(true, || {
|
||||
Some(Err(crate::camera::FrustumError::Degenerate { plane: 2 }))
|
||||
})
|
||||
.unwrap_err();
|
||||
@@ -1265,10 +1276,11 @@ pub struct Renderer<T: scene::Scene> {
|
||||
snapshot_init_sent: bool,
|
||||
scene_frame: scene_frame::SceneFrameCache,
|
||||
gpu_scene: gpu_scene::GpuSceneCache,
|
||||
instance_traversal: Option<instance_traversal::TraversalGpu>,
|
||||
materials: material::MaterialResources,
|
||||
pub(crate) command_ring: Option<&'static CommandRing>,
|
||||
pending_replies: Vec<JsValue>,
|
||||
gpu_error: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
gpu_error: std::sync::Arc<std::sync::Mutex<Option<String>>>,
|
||||
framing_radius: f32,
|
||||
graph_registry: crate::render_graph::Registry,
|
||||
active_compiled: Option<ActiveCompiledGraph>,
|
||||
@@ -1471,19 +1483,39 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
let result = js_sys::Object::new();
|
||||
let meshes = js_sys::Array::new();
|
||||
for h in installed.meshes {
|
||||
meshes.push(&js_sys::Array::of2(
|
||||
&h.slot().into(),
|
||||
&h.generation().into(),
|
||||
));
|
||||
let item = js_sys::Object::new();
|
||||
js_sys::Reflect::set(
|
||||
&item,
|
||||
&"handle".into(),
|
||||
&js_sys::Array::of2(&h.slot().into(), &h.generation().into()),
|
||||
)
|
||||
.unwrap();
|
||||
let ty = self
|
||||
.render_data
|
||||
.mesh(h)
|
||||
.unwrap()
|
||||
.default_instance_type
|
||||
.words;
|
||||
js_sys::Reflect::set(
|
||||
&item,
|
||||
&"defaultType".into(),
|
||||
&js_sys::Array::from_iter(ty.into_iter().map(JsValue::from)),
|
||||
)
|
||||
.unwrap();
|
||||
meshes.push(&item);
|
||||
}
|
||||
js_sys::Reflect::set(&result, &"meshes".into(), &meshes).unwrap();
|
||||
Ok(result.into())
|
||||
}
|
||||
2 => {
|
||||
self.render_data
|
||||
.set_mesh_flags(
|
||||
.set_mesh_visible(
|
||||
MeshHandle::from_parts(words[2], words[3]),
|
||||
RenderFlags::from_bits_retain(words[4]),
|
||||
match words[4] {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => return Err("INVALID_VISIBILITY"),
|
||||
},
|
||||
)
|
||||
.map_err(|e| render_data_error_code(&e))?;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
@@ -1496,15 +1528,25 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
}
|
||||
let h = self
|
||||
.render_data
|
||||
.create_instance(mesh, m, RenderFlags::from_bits_retain(words[20]))
|
||||
.create_instance(
|
||||
mesh,
|
||||
m,
|
||||
InstanceType {
|
||||
words: std::array::from_fn(|i| words[20 + i]),
|
||||
},
|
||||
)
|
||||
.map_err(|e| render_data_error_code(&e))?;
|
||||
Ok(js_sys::Array::of2(&h.slot().into(), &h.generation().into()).into())
|
||||
}
|
||||
4 => {
|
||||
self.render_data
|
||||
.set_instance_flags(
|
||||
.set_instance_visible(
|
||||
InstanceHandle::from_parts(words[2], words[3]),
|
||||
RenderFlags::from_bits_retain(words[4]),
|
||||
match words[4] {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => return Err("INVALID_VISIBILITY"),
|
||||
},
|
||||
)
|
||||
.map_err(|e| render_data_error_code(&e))?;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
@@ -1526,6 +1568,17 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.map_err(|e| render_data_error_code(&e))?;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
}
|
||||
10 => {
|
||||
self.render_data
|
||||
.set_instance_type(
|
||||
InstanceHandle::from_parts(words[2], words[3]),
|
||||
InstanceType {
|
||||
words: std::array::from_fn(|i| words[4 + i]),
|
||||
},
|
||||
)
|
||||
.map_err(|e| render_data_error_code(&e))?;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
}
|
||||
_ => Err("UNKNOWN_OPCODE"),
|
||||
})();
|
||||
match outcome {
|
||||
@@ -1765,8 +1818,7 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
let contract = crate::render_graph::contract(&execution.executor.key)
|
||||
.ok_or_else(|| fail("executor contract missing"))?;
|
||||
match execution.executor.key.as_str() {
|
||||
"frustum_cull" => executions.push(PreparedExecution::FrustumCull),
|
||||
"mesh_query" => executions.push(PreparedExecution::MeshQuery),
|
||||
"frustum_cull" => continue,
|
||||
_ if execution.executor.key == "frame_out"
|
||||
|| contract.fullscreen_policy.is_some() =>
|
||||
{
|
||||
@@ -1924,7 +1976,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
_uniform: uniform,
|
||||
});
|
||||
}
|
||||
"pipeline_registry" => executions.push(PreparedExecution::PipelineRegistry),
|
||||
"pipeline" => {
|
||||
let ExecutionKind::Render {
|
||||
color_attachments,
|
||||
@@ -2009,6 +2060,14 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
executions.push(PreparedExecution::Pipeline {
|
||||
execution: index,
|
||||
base,
|
||||
predicate_ordinal: runtime
|
||||
.instance_traversal
|
||||
.as_ref()
|
||||
.and_then(|p| {
|
||||
p.pipelines.iter().find(|v| v.execution as usize == index)
|
||||
})
|
||||
.map(|p| p.ordinal)
|
||||
.ok_or_else(|| fail("pipeline predicate missing"))?,
|
||||
variant,
|
||||
});
|
||||
}
|
||||
@@ -2049,7 +2108,13 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
// Candidate construction allocates GPU resources, so the live scene preflight
|
||||
// belongs here: this is the earliest boundary with both the runtime query and
|
||||
// scene access, and precedes GPU work and all pending/in-flight mutation.
|
||||
resolve_culling_frustum(runtime.allocations.query, || self.scene.frustum_planes())?;
|
||||
resolve_culling_frustum(
|
||||
runtime
|
||||
.instance_traversal
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.requires_camera),
|
||||
|| self.scene.frustum_planes(),
|
||||
)?;
|
||||
let restart_graph = graph.clone();
|
||||
self.next_preparation_token = self.next_preparation_token.wrapping_add(1).max(1);
|
||||
let token = self.next_preparation_token;
|
||||
@@ -2153,9 +2218,13 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let optional_features = profiler::Profiler::requested_features(profile, adapter.features());
|
||||
let feature_plan = device_feature_plan(profile, adapter.features());
|
||||
assert!(
|
||||
adapter.features().contains(feature_plan.hard),
|
||||
"WebGPU adapter lacks required indirect-first-instance support"
|
||||
);
|
||||
let descriptor = wgpu::DeviceDescriptor {
|
||||
required_features: optional_features,
|
||||
required_features: feature_plan.initial,
|
||||
required_limits: wgpu::Limits::default(),
|
||||
label: None,
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
@@ -2164,7 +2233,7 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
|
||||
let (device, queue) = match adapter.request_device(&descriptor).await {
|
||||
Ok(result) => result,
|
||||
Err(error) if !optional_features.is_empty() => {
|
||||
Err(error) if feature_plan.profiling_enabled => {
|
||||
log::warn!("timestamp-enabled device request failed, retrying baseline: {error}");
|
||||
adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
@@ -2174,8 +2243,12 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
})
|
||||
.await
|
||||
.expect("surface-compatible adapter required for baseline device");
|
||||
assert!(
|
||||
adapter.features().contains(feature_plan.hard),
|
||||
"reacquired WebGPU adapter lacks required indirect-first-instance support"
|
||||
);
|
||||
let baseline = wgpu::DeviceDescriptor {
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_features: feature_plan.hard,
|
||||
required_limits: wgpu::Limits::default(),
|
||||
label: None,
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
@@ -2185,15 +2258,23 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
}
|
||||
Err(error) => panic!("baseline WebGPU device request failed: {error}"),
|
||||
};
|
||||
assert!(
|
||||
device.features().contains(feature_plan.hard),
|
||||
"WebGPU device lacks required indirect-first-instance support"
|
||||
);
|
||||
info!("Adapter info: {:?}", adapter.get_info());
|
||||
info!("Adapter features: {:?}", adapter.features());
|
||||
info!("Adapter limits: {:?}", adapter.limits());
|
||||
let profiler = profiler::Profiler::new(profile, &device, &queue).await;
|
||||
let gpu_error = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let gpu_error = std::sync::Arc::new(std::sync::Mutex::new(None));
|
||||
let error_flag = gpu_error.clone();
|
||||
device.on_uncaptured_error(Box::new(move |error| {
|
||||
error_flag.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
log::error!("Uncaptured GPU error: {error}");
|
||||
let message = error.to_string();
|
||||
let mut first = error_flag.lock().unwrap();
|
||||
if first.is_none() {
|
||||
*first = Some(message.clone());
|
||||
}
|
||||
log::error!("Uncaptured GPU error: {message}");
|
||||
}));
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
@@ -2244,6 +2325,7 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
snapshot_init_sent: false,
|
||||
scene_frame: Default::default(),
|
||||
gpu_scene: Default::default(),
|
||||
instance_traversal: None,
|
||||
materials,
|
||||
command_ring: None,
|
||||
pending_replies: Vec::new(),
|
||||
@@ -2266,12 +2348,10 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
return;
|
||||
}
|
||||
self.drain_preparation_completions();
|
||||
if self
|
||||
.gpu_error
|
||||
.swap(false, std::sync::atomic::Ordering::AcqRel)
|
||||
{
|
||||
let gpu_error = self.gpu_error.lock().unwrap().take();
|
||||
if let Some(error) = gpu_error {
|
||||
self.halted = true;
|
||||
self.post_fatal("GPU_VALIDATION_FAILED", "uncaptured WebGPU error");
|
||||
self.post_fatal("GPU_VALIDATION_FAILED", &error);
|
||||
return;
|
||||
}
|
||||
if !self.drain_commands() {
|
||||
@@ -2295,8 +2375,16 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
&"controlPtr".into(),
|
||||
&self.snapshot.control_ptr().into(),
|
||||
);
|
||||
let _ = js_sys::Reflect::set(&message, &"controlVersion".into(), &1.into());
|
||||
let _ = js_sys::Reflect::set(&message, &"schemaVersion".into(), &1.into());
|
||||
let _ = js_sys::Reflect::set(
|
||||
&message,
|
||||
&"controlVersion".into(),
|
||||
&crate::shared_snapshot::CONTROL_VERSION.into(),
|
||||
);
|
||||
let _ = js_sys::Reflect::set(
|
||||
&message,
|
||||
&"schemaVersion".into(),
|
||||
&crate::shared_snapshot::SCHEMA.into(),
|
||||
);
|
||||
let _ = global.post_message(&message);
|
||||
self.snapshot_init_sent = true;
|
||||
}
|
||||
@@ -2351,26 +2439,14 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let upload = if let Some(query) = query {
|
||||
self.gpu_scene.upload_with_query(
|
||||
&self.context.device,
|
||||
&self.context.queue,
|
||||
frame_plan,
|
||||
query,
|
||||
)
|
||||
} else {
|
||||
self.gpu_scene
|
||||
.upload(&self.context.device, &self.context.queue, frame_plan)
|
||||
};
|
||||
let upload = self
|
||||
.gpu_scene
|
||||
.upload(&self.context.device, &self.context.queue, frame_plan);
|
||||
if let Err(error) = upload {
|
||||
log::error!("GPU scene upload failed: {error}");
|
||||
self.post_fatal("GPU_UPLOAD_FAILED", &error);
|
||||
return;
|
||||
}
|
||||
if let Some(query) = query {
|
||||
self.gpu_scene
|
||||
.write_culling_params(&self.context.queue, planes, query);
|
||||
}
|
||||
|
||||
// Candidate publication is transactional: configure its complete contract at
|
||||
// the last possible point before acquisition, but retain the known-good
|
||||
@@ -2463,6 +2539,41 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
),
|
||||
});
|
||||
let encode_result = if let Some(active) = rendering_compiled {
|
||||
(|| -> Result<(), &'static str> {
|
||||
let plan = active
|
||||
.runtime
|
||||
.instance_traversal
|
||||
.as_ref()
|
||||
.ok_or("compiled graph instance traversal missing")?;
|
||||
let rebuild = self.instance_traversal.as_ref().is_none_or(|traversal| {
|
||||
!traversal.matches(
|
||||
active.id,
|
||||
plan,
|
||||
self.gpu_scene.buffer_epoch,
|
||||
self.gpu_scene.draws.len(),
|
||||
)
|
||||
});
|
||||
if rebuild {
|
||||
self.instance_traversal = Some(
|
||||
instance_traversal::TraversalGpu::create(
|
||||
&self.context.device,
|
||||
active.id,
|
||||
plan,
|
||||
&self.gpu_scene,
|
||||
)
|
||||
.map_err(|error| {
|
||||
log::error!("instance traversal preparation failed: {error}");
|
||||
"instance traversal preparation failed"
|
||||
})?,
|
||||
);
|
||||
}
|
||||
self.instance_traversal.as_ref().unwrap().encode(
|
||||
&mut encoder,
|
||||
&self.context.queue,
|
||||
planes,
|
||||
self.gpu_scene.draws.len() as u32,
|
||||
profile_frame.as_mut(),
|
||||
);
|
||||
executors::encode_compiled(
|
||||
&mut encoder,
|
||||
&texture_view,
|
||||
@@ -2471,8 +2582,10 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
&self.gpu_scene,
|
||||
&self.resources,
|
||||
&self.materials,
|
||||
&self.instance_traversal.as_ref().unwrap().commands,
|
||||
profile_frame.as_mut(),
|
||||
)
|
||||
})()
|
||||
} else {
|
||||
executors::encode_immediate(
|
||||
&mut encoder,
|
||||
@@ -2620,12 +2733,7 @@ impl<T: Scene + 'static> Renderer<T> {
|
||||
.unwrap_or(0)
|
||||
.into(),
|
||||
),
|
||||
(
|
||||
"gpuError",
|
||||
self.gpu_error
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
.into(),
|
||||
),
|
||||
("gpuError", self.gpu_error.lock().unwrap().is_some().into()),
|
||||
] {
|
||||
let _ = js_sys::Reflect::set(&telemetry, &key.into(), &value);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ pub struct PipelineLibrary {
|
||||
default_layout: Option<PipelineLayoutKey>,
|
||||
material_layout: Option<PipelineLayoutKey>,
|
||||
next_layout: u64,
|
||||
pipeline_registry: HashMap<String, (PipelineKey, RenderPipelineKey)>,
|
||||
named_bases: HashMap<String, (PipelineKey, RenderPipelineKey)>,
|
||||
descriptor_cache: HashMap<RenderPipelineKey, PipelineKey>,
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ impl PipelineLibrary {
|
||||
default_layout: None,
|
||||
material_layout: None,
|
||||
next_layout: 0,
|
||||
pipeline_registry: HashMap::new(),
|
||||
named_bases: HashMap::new(),
|
||||
descriptor_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@ impl PipelineLibrary {
|
||||
) -> Result<PipelineKey, String> {
|
||||
let spec = self.compatibility_spec(name, layouts, shader, format);
|
||||
let descriptor = spec.key();
|
||||
if let Some((_, existing)) = self.pipeline_registry.get(name) {
|
||||
if let Some((_, existing)) = self.named_bases.get(name) {
|
||||
return Err(if existing == &descriptor {
|
||||
format!("Pipeline '{name}' already exists")
|
||||
} else {
|
||||
@@ -336,13 +336,12 @@ impl PipelineLibrary {
|
||||
});
|
||||
}
|
||||
let key = self.get_or_create_from_spec(device, &spec, Some(name));
|
||||
self.pipeline_registry
|
||||
.insert(name.to_owned(), (key, descriptor));
|
||||
self.named_bases.insert(name.to_owned(), (key, descriptor));
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn find_pipeline(&self, name: &str) -> Option<PipelineKey> {
|
||||
self.pipeline_registry.get(name).map(|v| v.0)
|
||||
self.named_bases.get(name).map(|v| v.0)
|
||||
}
|
||||
pub fn get_or_create_pipeline(
|
||||
&mut self,
|
||||
@@ -353,7 +352,7 @@ impl PipelineLibrary {
|
||||
format: wgpu::TextureFormat,
|
||||
) -> PipelineKey {
|
||||
let wanted = self.compatibility_spec(name, layouts, shader, format).key();
|
||||
if let Some((key, existing)) = self.pipeline_registry.get(name) {
|
||||
if let Some((key, existing)) = self.named_bases.get(name) {
|
||||
assert_eq!(
|
||||
existing, &wanted,
|
||||
"Pipeline '{name}' requested with a different descriptor"
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::render_data::{
|
||||
affine_world_aabb, Aabb, GeometryRange, InstanceHandle, MaterialKey, MeshHandle,
|
||||
ModelTransform, NormalMatrix, PipelineKey, RenderData, RenderFlags,
|
||||
affine_world_aabb, Aabb, GeometryRange, InstanceHandle, InstanceType, MaterialKey, MeshHandle,
|
||||
ModelTransform, NormalMatrix, PipelineKey, RenderData,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -13,8 +13,8 @@ pub struct SceneFrameMesh {
|
||||
pub geometry: GeometryRange,
|
||||
pub pipeline: PipelineKey,
|
||||
pub material: MaterialKey,
|
||||
pub flags: RenderFlags,
|
||||
pub aabb: Aabb,
|
||||
pub instance_type: InstanceType,
|
||||
pub local_aabb: Aabb,
|
||||
pub default_instance: InstanceHandle,
|
||||
pub occurrence_range: std::ops::Range<usize>,
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub struct SceneFrameOccurrence {
|
||||
pub mesh_index: usize,
|
||||
pub model: ModelTransform,
|
||||
pub normal: NormalMatrix,
|
||||
pub flags: RenderFlags,
|
||||
pub instance_type: InstanceType,
|
||||
pub is_default: bool,
|
||||
pub world_aabb: Aabb,
|
||||
}
|
||||
@@ -84,9 +84,9 @@ impl SceneFramePlan {
|
||||
mesh_index,
|
||||
model: occurrence.model,
|
||||
normal: occurrence.normal,
|
||||
flags: occurrence.flags,
|
||||
instance_type: occurrence.instance_type,
|
||||
is_default: handle == mesh.default_instance,
|
||||
world_aabb: affine_world_aabb(mesh.aabb, occurrence.model)
|
||||
world_aabb: affine_world_aabb(mesh.local_aabb, occurrence.model)
|
||||
.map_err(|_| SceneFrameError::InvalidWorldBounds)?,
|
||||
});
|
||||
}
|
||||
@@ -117,8 +117,8 @@ impl SceneFramePlan {
|
||||
geometry: mesh.geometry,
|
||||
pipeline: mesh.pipeline,
|
||||
material: mesh.material,
|
||||
flags: mesh.flags,
|
||||
aabb: mesh.aabb,
|
||||
instance_type: mesh.default_instance_type,
|
||||
local_aabb: mesh.local_aabb,
|
||||
default_instance: mesh.default_instance,
|
||||
occurrence_range: offsets[dense]..offsets[dense + 1],
|
||||
})
|
||||
@@ -170,12 +170,11 @@ mod tests {
|
||||
indices: &[0, 1, 2],
|
||||
pipeline: PipelineKey::new(0),
|
||||
material: crate::render_data::MaterialKey::DEFAULT,
|
||||
flags: if visible {
|
||||
RenderFlags::VISIBLE
|
||||
default_instance_type: if visible {
|
||||
InstanceType::VISIBLE
|
||||
} else {
|
||||
RenderFlags::NONE
|
||||
InstanceType::ZERO
|
||||
},
|
||||
default_instance_flags: RenderFlags::VISIBLE,
|
||||
default_transform: IDENTITY_MODEL_TRANSFORM,
|
||||
})
|
||||
.unwrap()
|
||||
@@ -190,8 +189,7 @@ mod tests {
|
||||
let created = mesh(&mut data, true);
|
||||
let second = cache.get_or_build(&data).unwrap() as *const _;
|
||||
assert_ne!(first, second);
|
||||
data.set_mesh_flags(created.mesh, RenderFlags::NONE)
|
||||
.unwrap();
|
||||
data.set_mesh_visible(created.mesh, false).unwrap();
|
||||
let third = cache.get_or_build(&data).unwrap() as *const _;
|
||||
assert_ne!(second, third);
|
||||
let mut moved = IDENTITY_MODEL_TRANSFORM;
|
||||
@@ -213,7 +211,7 @@ mod tests {
|
||||
translated[3][0] = 5.;
|
||||
translated[3][1] = -2.;
|
||||
let extra = data
|
||||
.create_instance(hidden.mesh, translated, RenderFlags::NONE)
|
||||
.create_instance(hidden.mesh, translated, InstanceType::ZERO)
|
||||
.unwrap();
|
||||
let plan = SceneFramePlan::build(&data).unwrap();
|
||||
assert_eq!((plan.meshes.len(), plan.occurrences.len()), (2, 3));
|
||||
@@ -247,13 +245,13 @@ mod tests {
|
||||
let doomed = mesh(&mut data, true);
|
||||
let c = mesh(&mut data, true);
|
||||
let a_extra = data
|
||||
.create_instance(a.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::VISIBLE)
|
||||
.create_instance(a.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::VISIBLE)
|
||||
.unwrap();
|
||||
let doomed_extra = data
|
||||
.create_instance(doomed.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::VISIBLE)
|
||||
.create_instance(doomed.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::VISIBLE)
|
||||
.unwrap();
|
||||
let c_extra = data
|
||||
.create_instance(c.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::VISIBLE)
|
||||
.create_instance(c.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::VISIBLE)
|
||||
.unwrap();
|
||||
data.destroy_instance(a_extra).unwrap();
|
||||
data.destroy_mesh(doomed.mesh).unwrap();
|
||||
@@ -262,7 +260,7 @@ mod tests {
|
||||
.create_instance(
|
||||
replacement.mesh,
|
||||
IDENTITY_MODEL_TRANSFORM,
|
||||
RenderFlags::VISIBLE,
|
||||
InstanceType::VISIBLE,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(replacement.mesh.slot(), doomed.mesh.slot());
|
||||
|
||||
+59
-259
@@ -1,12 +1,12 @@
|
||||
//! Triple-buffered, immutable packed scene snapshot shared with JavaScript.
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::{render_data::RenderFlags, renderer::scene_frame::SceneFramePlan};
|
||||
use crate::renderer::scene_frame::SceneFramePlan;
|
||||
|
||||
pub const MAGIC: u32 = u32::from_le_bytes(*b"YSNP");
|
||||
pub const BLOB_MAGIC: u32 = u32::from_le_bytes(*b"RDS1");
|
||||
pub const BLOB_MAGIC: u32 = u32::from_le_bytes(*b"RDS2");
|
||||
pub const CONTROL_VERSION: u32 = 1;
|
||||
pub const SCHEMA: u32 = 1;
|
||||
pub const SCHEMA: u32 = 2;
|
||||
pub const SLOT_COUNT: usize = 3;
|
||||
pub const INIT: u32 = 0;
|
||||
pub const OPEN: u32 = 1;
|
||||
@@ -23,9 +23,9 @@ pub const ERROR_PUBLICATION: u32 = 4;
|
||||
const CONTROL_BYTES: u32 = 256;
|
||||
const SLOT_BYTES: u32 = 64;
|
||||
const SNAPSHOT_HEADER_BYTES: usize = 64;
|
||||
const DATA_OFFSET: usize = 512;
|
||||
const DATA_OFFSET: usize = 448;
|
||||
const DESCRIPTOR_BYTES: usize = 32;
|
||||
const STREAMS: usize = 14;
|
||||
const STREAMS: usize = 12;
|
||||
const SCHEMA_FLAGS: u32 = 3; // dense arrays | affine transforms
|
||||
|
||||
#[repr(C, align(64))]
|
||||
@@ -247,41 +247,40 @@ fn wasm_pages(minimum_end: usize) -> Result<u32, u32> {
|
||||
fn pack(data: &SceneFramePlan, epoch: u32) -> Result<Vec<u8>, u32> {
|
||||
let meshes = &data.meshes;
|
||||
let instances = &data.occurrences;
|
||||
let strides = [4usize, 4, 4, 12, 12, 4, 4, 4, 4, 4, 64, 12, 12, 4];
|
||||
let components = [1u32, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
|
||||
let scalar = [1u32, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
let counts = [meshes.len(); 5]
|
||||
let strides = [4usize, 4, 12, 12, 4, 4, 4, 4, 64, 12, 12, 64];
|
||||
let components = [1u32, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
|
||||
let scalar = [1u32, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
let counts = [meshes.len(); 4]
|
||||
.into_iter()
|
||||
.chain([instances.len(); 9])
|
||||
.chain([instances.len(); 8])
|
||||
.collect::<Vec<_>>();
|
||||
let mut offsets = [0usize; STREAMS];
|
||||
let mut cursor = DATA_OFFSET;
|
||||
for i in 0..STREAMS {
|
||||
offsets[i] = cursor;
|
||||
let bytes = strides[i].checked_mul(counts[i]).ok_or(ERROR_OVERFLOW)?;
|
||||
cursor = align16(cursor.checked_add(bytes).ok_or(ERROR_OVERFLOW)?)?;
|
||||
cursor = align16(
|
||||
cursor
|
||||
.checked_add(strides[i].checked_mul(counts[i]).ok_or(ERROR_OVERFLOW)?)
|
||||
.ok_or(ERROR_OVERFLOW)?,
|
||||
)?;
|
||||
}
|
||||
let total = u32::try_from(cursor).map_err(|_| ERROR_OVERFLOW)?;
|
||||
let mesh_count = u32::try_from(meshes.len()).map_err(|_| ERROR_OVERFLOW)?;
|
||||
let instance_count = u32::try_from(instances.len()).map_err(|_| ERROR_OVERFLOW)?;
|
||||
let mut out = vec![0u8; cursor];
|
||||
let put32 = |out: &mut [u8], at: usize, value: u32| {
|
||||
out[at..at + 4].copy_from_slice(&value.to_le_bytes())
|
||||
};
|
||||
let put32 =
|
||||
|out: &mut [u8], at: usize, v: u32| out[at..at + 4].copy_from_slice(&v.to_le_bytes());
|
||||
let revision = data.revision;
|
||||
for (i, value) in [
|
||||
for (i, v) in [
|
||||
BLOB_MAGIC,
|
||||
SCHEMA,
|
||||
SNAPSHOT_HEADER_BYTES as u32,
|
||||
total,
|
||||
64,
|
||||
cursor as u32,
|
||||
epoch,
|
||||
revision as u32,
|
||||
(revision >> 32) as u32,
|
||||
STREAMS as u32,
|
||||
SNAPSHOT_HEADER_BYTES as u32,
|
||||
DESCRIPTOR_BYTES as u32,
|
||||
mesh_count,
|
||||
instance_count,
|
||||
64,
|
||||
32,
|
||||
meshes.len() as u32,
|
||||
instances.len() as u32,
|
||||
0x0102_0304,
|
||||
SCHEMA_FLAGS,
|
||||
0,
|
||||
@@ -290,11 +289,11 @@ fn pack(data: &SceneFramePlan, epoch: u32) -> Result<Vec<u8>, u32> {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
put32(&mut out, i * 4, value);
|
||||
put32(&mut out, i * 4, v);
|
||||
}
|
||||
for i in 0..STREAMS {
|
||||
let at = SNAPSHOT_HEADER_BYTES + i * DESCRIPTOR_BYTES;
|
||||
for (j, value) in [
|
||||
let at = 64 + i * 32;
|
||||
for (j, v) in [
|
||||
i as u32 + 1,
|
||||
scalar[i],
|
||||
offsets[i] as u32,
|
||||
@@ -307,75 +306,61 @@ fn pack(data: &SceneFramePlan, epoch: u32) -> Result<Vec<u8>, u32> {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
put32(&mut out, at + j * 4, value);
|
||||
put32(&mut out, at + j * 4, v);
|
||||
}
|
||||
}
|
||||
for (dense, mesh) in meshes.iter().enumerate() {
|
||||
for (i, value) in [
|
||||
mesh.handle.slot(),
|
||||
mesh.handle.generation(),
|
||||
mesh.flags.bits(),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
put32(&mut out, offsets[i] + dense * 4, value);
|
||||
}
|
||||
for (d, m) in meshes.iter().enumerate() {
|
||||
put32(&mut out, offsets[0] + d * 4, m.handle.slot());
|
||||
put32(&mut out, offsets[1] + d * 4, m.handle.generation());
|
||||
for i in 0..3 {
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[3] + dense * 12 + i * 4,
|
||||
mesh.aabb.min[i].to_bits(),
|
||||
offsets[2] + d * 12 + i * 4,
|
||||
m.local_aabb.min[i].to_bits(),
|
||||
);
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[4] + dense * 12 + i * 4,
|
||||
mesh.aabb.max[i].to_bits(),
|
||||
offsets[3] + d * 12 + i * 4,
|
||||
m.local_aabb.max[i].to_bits(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (dense, instance) in instances.iter().enumerate() {
|
||||
let mesh = data
|
||||
.meshes
|
||||
.get(instance.mesh_index)
|
||||
.ok_or(ERROR_INVARIANT)?;
|
||||
for (i, value) in [
|
||||
instance.handle.slot(),
|
||||
instance.handle.generation(),
|
||||
instance.mesh.slot(),
|
||||
instance.mesh.generation(),
|
||||
instance.flags.bits(),
|
||||
for (d, x) in instances.iter().enumerate() {
|
||||
for (i, v) in [
|
||||
x.handle.slot(),
|
||||
x.handle.generation(),
|
||||
x.mesh.slot(),
|
||||
x.mesh.generation(),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
put32(&mut out, offsets[5 + i] + dense * 4, value);
|
||||
put32(&mut out, offsets[4 + i] + d * 4, v);
|
||||
}
|
||||
for i in 0..16 {
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[10] + dense * 64 + i * 4,
|
||||
instance.model[i / 4][i % 4].to_bits(),
|
||||
offsets[8] + d * 64 + i * 4,
|
||||
x.model[i / 4][i % 4].to_bits(),
|
||||
);
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[11] + d * 64 + i * 4,
|
||||
x.instance_type.words[i],
|
||||
);
|
||||
}
|
||||
for i in 0..3 {
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[11] + dense * 12 + i * 4,
|
||||
instance.world_aabb.min[i].to_bits(),
|
||||
offsets[9] + d * 12 + i * 4,
|
||||
x.world_aabb.min[i].to_bits(),
|
||||
);
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[12] + dense * 12 + i * 4,
|
||||
instance.world_aabb.max[i].to_bits(),
|
||||
offsets[10] + d * 12 + i * 4,
|
||||
x.world_aabb.max[i].to_bits(),
|
||||
);
|
||||
}
|
||||
put32(
|
||||
&mut out,
|
||||
offsets[13] + dense * 4,
|
||||
(mesh.flags.contains(RenderFlags::VISIBLE)
|
||||
&& instance.flags.contains(RenderFlags::VISIBLE)) as u32,
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -393,199 +378,14 @@ const _: [(); 256] = [(); std::mem::size_of::<SnapshotControl>()];
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render_data::{
|
||||
MeshCreateInfo, PipelineKey, RenderData, RenderDataConfig, IDENTITY_MODEL_TRANSFORM,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn exact_control_layout_and_initial_values() {
|
||||
fn exact_schema_two_layout() {
|
||||
assert_eq!(BLOB_MAGIC, u32::from_le_bytes(*b"RDS2"));
|
||||
assert_eq!(SCHEMA, 2);
|
||||
assert_eq!(STREAMS, 12);
|
||||
assert_eq!(DATA_OFFSET, 448);
|
||||
assert_eq!(std::mem::size_of::<SnapshotControl>(), 256);
|
||||
assert_eq!(std::mem::size_of::<SnapshotDescriptor>(), 64);
|
||||
let snapshot = SharedSnapshot::new();
|
||||
let values: Vec<_> = snapshot
|
||||
.control
|
||||
.header
|
||||
.iter()
|
||||
.map(|v| v.load(Ordering::Relaxed))
|
||||
.collect();
|
||||
assert_eq!(&values[..7], &[MAGIC, 1, 256, 3, 64, 1, INIT]);
|
||||
assert_eq!(values[9], u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claiming_never_overwrites_reading_and_prefers_free() {
|
||||
let snapshot = SharedSnapshot::new();
|
||||
snapshot.control.slots[0].0[0].store(READING, Ordering::Relaxed);
|
||||
assert_eq!(snapshot.claim_slot(), Some(1));
|
||||
assert_eq!(
|
||||
snapshot.control.slots[0].0[0].load(Ordering::Relaxed),
|
||||
READING
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.control.slots[1].0[0].load(Ordering::Relaxed),
|
||||
WRITING
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn producer_blob_abi_matches_schema_exactly() {
|
||||
let mut data = RenderData::new(RenderDataConfig::default()).unwrap();
|
||||
let create = |data: &mut RenderData, flags, x: f32| {
|
||||
data.create_mesh(MeshCreateInfo {
|
||||
positions: &[[x, 0., 0.], [x + 2., 0., 0.], [x, 3., 0.]],
|
||||
normals: &[[0., 0., 1.]; 3],
|
||||
tangents: &[[1., 0., 0., 1.]; 3],
|
||||
uvs: &[[0., 0.]; 3],
|
||||
indices: &[0, 1, 2],
|
||||
pipeline: PipelineKey::new(7),
|
||||
material: crate::render_data::MaterialKey::DEFAULT,
|
||||
flags,
|
||||
default_instance_flags: RenderFlags::VISIBLE,
|
||||
default_transform: IDENTITY_MODEL_TRANSFORM,
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
let visible = create(&mut data, RenderFlags::VISIBLE, -1.0);
|
||||
let hidden = create(&mut data, RenderFlags::NONE, 10.0);
|
||||
let mut model = IDENTITY_MODEL_TRANSFORM;
|
||||
model[0][0] = 2.0;
|
||||
model[1][1] = 0.5;
|
||||
model[3][0] = 4.0;
|
||||
model[3][1] = -2.0;
|
||||
let extra = data
|
||||
.create_instance(hidden.mesh, model, RenderFlags::NONE)
|
||||
.unwrap();
|
||||
let plan = SceneFramePlan::build(&data).unwrap();
|
||||
let epoch = 0x1234_5678;
|
||||
let blob = pack(&plan, epoch).unwrap();
|
||||
let word = |at: usize| u32::from_le_bytes(blob[at..at + 4].try_into().unwrap());
|
||||
|
||||
assert_eq!(word(0), BLOB_MAGIC);
|
||||
assert_eq!(word(4), SCHEMA);
|
||||
assert_eq!(word(8), SNAPSHOT_HEADER_BYTES as u32);
|
||||
assert_eq!(word(12), blob.len() as u32);
|
||||
assert_eq!(word(16), epoch);
|
||||
assert_eq!(word(20), plan.revision as u32);
|
||||
assert_eq!(word(24), (plan.revision >> 32) as u32);
|
||||
assert_eq!(word(28), STREAMS as u32);
|
||||
assert_eq!(word(32), SNAPSHOT_HEADER_BYTES as u32);
|
||||
assert_eq!(word(36), DESCRIPTOR_BYTES as u32);
|
||||
assert_eq!(word(40), 2);
|
||||
assert_eq!(word(44), 3);
|
||||
assert_eq!(word(48), 0x0102_0304);
|
||||
assert_eq!(word(52), SCHEMA_FLAGS);
|
||||
|
||||
let strides = [4, 4, 4, 12, 12, 4, 4, 4, 4, 4, 64, 12, 12, 4];
|
||||
let scalars = [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
let components = [1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
|
||||
let counts = [2usize; 5]
|
||||
.into_iter()
|
||||
.chain([3usize; 9])
|
||||
.collect::<Vec<_>>();
|
||||
let mut offsets = Vec::new();
|
||||
let mut cursor = DATA_OFFSET;
|
||||
for i in 0..STREAMS {
|
||||
let at = SNAPSHOT_HEADER_BYTES + i * DESCRIPTOR_BYTES;
|
||||
offsets.push(cursor);
|
||||
assert_eq!(
|
||||
[
|
||||
word(at),
|
||||
word(at + 4),
|
||||
word(at + 8),
|
||||
word(at + 12),
|
||||
word(at + 16),
|
||||
word(at + 20),
|
||||
word(at + 24),
|
||||
word(at + 28)
|
||||
],
|
||||
[
|
||||
i as u32 + 1,
|
||||
scalars[i],
|
||||
cursor as u32,
|
||||
counts[i] as u32,
|
||||
components[i],
|
||||
strides[i] as u32,
|
||||
4,
|
||||
0
|
||||
]
|
||||
);
|
||||
assert_eq!(cursor % 16, 0);
|
||||
cursor = (cursor + strides[i] as usize * counts[i] + 15) & !15;
|
||||
}
|
||||
assert_eq!(cursor, blob.len());
|
||||
|
||||
for (dense, mesh) in plan.meshes.iter().enumerate() {
|
||||
assert_eq!(word(offsets[0] + dense * 4), mesh.handle.slot());
|
||||
assert_eq!(word(offsets[1] + dense * 4), mesh.handle.generation());
|
||||
assert_eq!(word(offsets[2] + dense * 4), mesh.flags.bits());
|
||||
for axis in 0..3 {
|
||||
assert_eq!(
|
||||
word(offsets[3] + dense * 12 + axis * 4),
|
||||
mesh.aabb.min[axis].to_bits()
|
||||
);
|
||||
assert_eq!(
|
||||
word(offsets[4] + dense * 12 + axis * 4),
|
||||
mesh.aabb.max[axis].to_bits()
|
||||
);
|
||||
}
|
||||
}
|
||||
for (dense, occurrence) in plan.occurrences.iter().enumerate() {
|
||||
assert_eq!(
|
||||
[
|
||||
word(offsets[5] + dense * 4),
|
||||
word(offsets[6] + dense * 4),
|
||||
word(offsets[7] + dense * 4),
|
||||
word(offsets[8] + dense * 4),
|
||||
word(offsets[9] + dense * 4)
|
||||
],
|
||||
[
|
||||
occurrence.handle.slot(),
|
||||
occurrence.handle.generation(),
|
||||
occurrence.mesh.slot(),
|
||||
occurrence.mesh.generation(),
|
||||
occurrence.flags.bits()
|
||||
]
|
||||
);
|
||||
for i in 0..16 {
|
||||
assert_eq!(
|
||||
word(offsets[10] + dense * 64 + i * 4),
|
||||
occurrence.model[i / 4][i % 4].to_bits()
|
||||
);
|
||||
}
|
||||
for axis in 0..3 {
|
||||
assert_eq!(
|
||||
word(offsets[11] + dense * 12 + axis * 4),
|
||||
occurrence.world_aabb.min[axis].to_bits()
|
||||
);
|
||||
assert_eq!(
|
||||
word(offsets[12] + dense * 12 + axis * 4),
|
||||
occurrence.world_aabb.max[axis].to_bits()
|
||||
);
|
||||
}
|
||||
let mesh_visible = plan.meshes[occurrence.mesh_index]
|
||||
.flags
|
||||
.contains(RenderFlags::VISIBLE);
|
||||
assert_eq!(
|
||||
word(offsets[13] + dense * 4),
|
||||
(mesh_visible && occurrence.flags.contains(RenderFlags::VISIBLE)) as u32
|
||||
);
|
||||
}
|
||||
assert!(plan
|
||||
.meshes
|
||||
.windows(2)
|
||||
.all(|w| w[0].handle.slot() < w[1].handle.slot()));
|
||||
assert!(plan
|
||||
.occurrences
|
||||
.windows(2)
|
||||
.all(|w| w[0].handle.slot() < w[1].handle.slot()));
|
||||
assert_eq!(
|
||||
plan.occurrences
|
||||
.iter()
|
||||
.find(|o| o.handle == extra)
|
||||
.unwrap()
|
||||
.model,
|
||||
model
|
||||
);
|
||||
assert!(plan.occurrences.iter().any(|o| o.mesh == visible.mesh));
|
||||
assert_eq!(snapshot.control.header[5].load(Ordering::Relaxed), 2);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ export class DerivedBvh {
|
||||
let changed = n !== this.count;
|
||||
if (!changed) for (let i = 0; i < n; i++) if (this.identity[i * 2] !== s.instanceSlot[i] || this.identity[i * 2 + 1] !== s.instanceGeneration[i] || this.meshIdentity[i * 2] !== s.instanceMeshSlot[i] || this.meshIdentity[i * 2 + 1] !== s.instanceMeshGeneration[i]) { changed = true; break; }
|
||||
this.count = n; this.identity = new Uint32Array(n * 2); this.meshIdentity = new Uint32Array(n * 2); this.pickable = new Uint8Array(n); this.bounds = new Float32Array(n * 6);
|
||||
for (let i = 0; i < n; i++) { this.identity.set([s.instanceSlot[i], s.instanceGeneration[i]], i * 2); this.meshIdentity.set([s.instanceMeshSlot[i], s.instanceMeshGeneration[i]], i * 2); this.pickable[i] = !!s.instancePickable[i]; this.bounds.set(s.instanceWorldMin.subarray(i * 3, i * 3 + 3), i * 6); this.bounds.set(s.instanceWorldMax.subarray(i * 3, i * 3 + 3), i * 6 + 3); }
|
||||
for (let i = 0; i < n; i++) { this.identity.set([s.instanceSlot[i], s.instanceGeneration[i]], i * 2); this.meshIdentity.set([s.instanceMeshSlot[i], s.instanceMeshGeneration[i]], i * 2); this.pickable[i] = !!(s.instanceType[i * 16] & 1); this.bounds.set(s.instanceWorldMin.subarray(i * 3, i * 3 + 3), i * 6); this.bounds.set(s.instanceWorldMax.subarray(i * 3, i * 3 + 3), i * 6 + 3); }
|
||||
changed ? this.rebuild() : this.refit();
|
||||
}
|
||||
rebuild() {
|
||||
|
||||
@@ -19,7 +19,7 @@ function coalescedUpdate(hint = 0) {
|
||||
addEventListener("message", event => {
|
||||
const m = event.data;
|
||||
try {
|
||||
if (m.type === "init") { reader = new SnapshotReader(m.memory, m.controlPtr); if (m.controlVersion !== 1 || m.schemaVersion !== 1) throw Object.assign(new Error("version"), {code: "PICK_PROTOCOL_MISMATCH"}); postMessage({type: "ready"}); }
|
||||
if (m.type === "init") { reader = new SnapshotReader(m.memory, m.controlPtr); if (m.controlVersion !== 1 || m.schemaVersion !== 2) throw Object.assign(new Error("version"), {code: "PICK_PROTOCOL_MISMATCH"}); postMessage({type: "ready"}); }
|
||||
else if (m.type === "update") coalescedUpdate(m.epoch);
|
||||
else if (m.type === "pick") { if (!ensureEpoch(m.epoch)) { postMessage({type: "pick", request: m.request, stale: true, epoch}); return; } const hits = bvh.pick(m.origin, m.direction, m.maxDistance, m.maxHits); const latest = reader.latest().epoch; postMessage({type: "pick", request: m.request, stale: latest !== m.epoch || epoch !== m.epoch, epoch, hits}); }
|
||||
else if (m.type === "dispose") close();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export const SNAPSHOT = Object.freeze({
|
||||
MAGIC: 0x504e5359, BLOB_MAGIC: 0x31534452, VERSION: 1, BYTES: 256,
|
||||
SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 1, INIT: 0, OPEN: 1, FAILED: 2,
|
||||
MAGIC: 0x504e5359, BLOB_MAGIC: 0x32534452, VERSION: 1, BYTES: 256,
|
||||
SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 2, INIT: 0, OPEN: 1, FAILED: 2,
|
||||
CLOSED: 3, FREE: 0, WRITING: 1, READY: 2, READING: 3,
|
||||
});
|
||||
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshFlags", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceFlags", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instancePickable"];
|
||||
const COMPONENTS = [1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
|
||||
const SCALARS = [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instanceType"];
|
||||
const COMPONENTS = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
|
||||
const SCALARS = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
const STRIDES = COMPONENTS.map(n => n * 4);
|
||||
|
||||
export class SnapshotProtocolError extends Error {
|
||||
@@ -70,15 +70,15 @@ export class SnapshotReader {
|
||||
if (slot[0] !== SNAPSHOT.READING || slot[1] !== latest.epoch || slot[2] !== latest.layoutEpoch || slot[5] !== latest.revisionLo || slot[6] !== latest.revisionHi || slot[9] !== SNAPSHOT.SCHEMA || slot[10] !== 64) return null;
|
||||
if (slot.slice(11).some(Boolean)) bad("BAD_SLOT_RESERVED");
|
||||
const ptr = slot[3], bytes = slot[4];
|
||||
if (ptr % 16 || bytes < 512 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
|
||||
if (ptr % 16 || bytes < 448 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
|
||||
const u32 = new Uint32Array(this.buffer, ptr, bytes / 4);
|
||||
if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 14 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
|
||||
if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 12 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
|
||||
if (u32[14] || u32[15]) bad("BAD_BLOB_RESERVED");
|
||||
const ranges = [], streams = {};
|
||||
for (let i = 0; i < 14; i++) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const d = 16 + i * 8, semantic = u32[d], scalar = u32[d + 1], offset = u32[d + 2], count = u32[d + 3], components = u32[d + 4], stride = u32[d + 5], width = u32[d + 6], reserved = u32[d + 7];
|
||||
const want = i < 5 ? slot[7] : slot[8];
|
||||
if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 512 || offset % 16) bad("BAD_DESCRIPTOR");
|
||||
const want = i < 4 ? slot[7] : slot[8];
|
||||
if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 448 || offset % 16) bad("BAD_DESCRIPTOR");
|
||||
const end = add(offset, mul(stride, count));
|
||||
if (end > bytes) bad("BAD_DESCRIPTOR_RANGE");
|
||||
if (count) ranges.push([offset, end]);
|
||||
|
||||
@@ -94,7 +94,7 @@ const mapValuePaths = (paths, path, source, value) => {
|
||||
mapValuePaths(paths, `${path}.${key}`, source, value[key]);
|
||||
};
|
||||
|
||||
function parameterValue(raw, schema, nodeId, key) {
|
||||
function parameterValue(raw, schema, nodeId, key, semanticType) {
|
||||
if (!exactKeys(raw, ["kind", "value"]) || raw.kind !== schema.type)
|
||||
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
const value = raw.value;
|
||||
@@ -112,13 +112,33 @@ function parameterValue(raw, schema, nodeId, key) {
|
||||
? typeof value === "boolean"
|
||||
: schema.type === "vector" || schema.type === "color"
|
||||
? Array.isArray(value) &&
|
||||
value.length === (schema.type === "vector" ? 3 : 4) &&
|
||||
value.length === (semanticType?.startsWith("vec") ? Number(semanticType.at(-1)) : (schema.type === "vector" ? 3 : 4)) &&
|
||||
value.every(bounded)
|
||||
: schema.type === "json" && finiteJson(value);
|
||||
: schema.type === "json" && finiteJson(value) && validSemanticValue(value, semanticType);
|
||||
if (!valid) fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
return canonical(structuredClone(raw.value));
|
||||
}
|
||||
|
||||
function validSemanticValue(value, type) {
|
||||
if (!type) return true;
|
||||
const finiteVector = (candidate, size) =>
|
||||
Array.isArray(candidate) && candidate.length === size && candidate.every(Number.isFinite);
|
||||
const vector = /^vec([24])$/.exec(type);
|
||||
if (vector) return finiteVector(value, Number(vector[1]));
|
||||
if (type === "u32x16")
|
||||
return Array.isArray(value) && value.length === 16 &&
|
||||
value.every((word) => Number.isInteger(word) && word >= 0 && word <= 0xffffffff);
|
||||
if (type === "local_aabb")
|
||||
return exactKeys(value, ["min", "max"]) && finiteVector(value.min, 3) && finiteVector(value.max, 3);
|
||||
const match = /^mat([234])$/.exec(type);
|
||||
if (match) {
|
||||
const size = Number(match[1]);
|
||||
return Array.isArray(value) && value.length === size &&
|
||||
value.every((column) => finiteVector(column, size));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
try {
|
||||
const rootKeys = [
|
||||
@@ -300,6 +320,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
socketDefinition.value,
|
||||
n.id,
|
||||
s.key,
|
||||
input.accepted.types[0],
|
||||
);
|
||||
return false;
|
||||
} catch {
|
||||
@@ -327,13 +348,16 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
}
|
||||
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
|
||||
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
|
||||
if (n.typeId === "mesh_query") {
|
||||
parameters.visibleDefault = sockets.get(
|
||||
`${n.id}:isVisible`,
|
||||
).defaultValue.value;
|
||||
parameters.frustumCulledDefault = sockets.get(
|
||||
`${n.id}:isFrustumCulled`,
|
||||
).defaultValue.value;
|
||||
for (const key of Object.keys(descriptor.inputs)) {
|
||||
const authoredDefault = sockets.get(`${n.id}:${key}`).defaultValue;
|
||||
if (authoredDefault)
|
||||
parameters[`${key}Default`] = parameterValue(
|
||||
authoredDefault,
|
||||
definition.sockets[key].value,
|
||||
n.id,
|
||||
key,
|
||||
descriptor.inputs[key].accepted.types[0],
|
||||
);
|
||||
}
|
||||
nodes.set(n.id, {
|
||||
ordinal,
|
||||
@@ -507,13 +531,12 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
mapValuePaths(
|
||||
paths,
|
||||
`${base}.parameters.${key}`,
|
||||
item.value.executor.key === "mesh_query" && key.endsWith("Default")
|
||||
key.endsWith("Default") && Object.hasOwn(descriptors[item.value.executor.key].inputs, key.slice(0, -7))
|
||||
? {
|
||||
kind: "input",
|
||||
nodeId: item.value.id,
|
||||
input:
|
||||
key === "visibleDefault" ? "isVisible" : "isFrustumCulled",
|
||||
socketId: `${item.value.id}:${key === "visibleDefault" ? "isVisible" : "isFrustumCulled"}`,
|
||||
input: key.slice(0, -7),
|
||||
socketId: `${item.value.id}:${key.slice(0, -7)}`,
|
||||
unconnected: true,
|
||||
}
|
||||
: parameterSource(
|
||||
|
||||
@@ -2,13 +2,14 @@ import { semanticCatalog } from "./catalog.js";
|
||||
|
||||
const GROUPS = Object.freeze([
|
||||
["source", "Source"],
|
||||
["expression", "Expression"],
|
||||
["compute", "Compute"],
|
||||
["cpu_preparation", "CPU preparation"],
|
||||
["render", "Render / post"],
|
||||
["frame", "Frame"],
|
||||
]);
|
||||
|
||||
const title = (typeId) => typeId.replaceAll("_", " ");
|
||||
const title = (typeId) => typeId.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
|
||||
/** Application-owned, immutable add-node catalog model. */
|
||||
export const addNodeItems = Object.freeze(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const GRAPH_ID = "authored_gpu_culling";
|
||||
export const CATALOG_VERSION = 7;
|
||||
export const CATALOG_VERSION = 8;
|
||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||
const i = (type, required = true, authoringType) => ({
|
||||
accepted: typeof type === "string" ? exact(type) : type,
|
||||
@@ -7,6 +7,45 @@ const i = (type, required = true, authoringType) => ({
|
||||
...(authoringType ? { authoringType } : {}),
|
||||
});
|
||||
const o = (type) => ({ type });
|
||||
const expression = (inputs, outputs) => ({
|
||||
version: 1,
|
||||
execution: "expression",
|
||||
inputs: Object.fromEntries(Object.entries(inputs).map(([name, type]) => [name, i(type, false)])),
|
||||
outputs: Object.fromEntries(Object.entries(outputs).map(([name, type]) => [name, o(type)])),
|
||||
parameters: {},
|
||||
});
|
||||
const numbered = (prefix, count, type) =>
|
||||
Object.fromEntries(Array.from({ length: count }, (_, index) => [`${prefix}${index}`, type]));
|
||||
const expressionCatalog = {
|
||||
and: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
or: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
not: expression({ operand: "bool" }, { value: "bool" }),
|
||||
xor: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
xnor: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
greater_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
|
||||
less_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
|
||||
equals_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
|
||||
greater_than_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }),
|
||||
less_than_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }),
|
||||
equals_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }),
|
||||
separate_vec2: expression({ vector: "vec2" }, { x: "f32", y: "f32" }),
|
||||
combine_vec2: expression({ x: "f32", y: "f32" }, { vector: "vec2" }),
|
||||
separate_vec3: expression({ vector: "vec3" }, { x: "f32", y: "f32", z: "f32" }),
|
||||
combine_vec3: expression({ x: "f32", y: "f32", z: "f32" }, { vector: "vec3" }),
|
||||
separate_vec4: expression({ vector: "vec4" }, { x: "f32", y: "f32", z: "f32", w: "f32" }),
|
||||
combine_vec4: expression({ x: "f32", y: "f32", z: "f32", w: "f32" }, { vector: "vec4" }),
|
||||
separate_mat2: expression({ matrix: "mat2" }, numbered("column", 2, "vec2")),
|
||||
combine_mat2: expression(numbered("column", 2, "vec2"), { matrix: "mat2" }),
|
||||
separate_mat3: expression({ matrix: "mat3" }, numbered("column", 3, "vec3")),
|
||||
combine_mat3: expression(numbered("column", 3, "vec3"), { matrix: "mat3" }),
|
||||
separate_mat4: expression({ matrix: "mat4" }, numbered("column", 4, "vec4")),
|
||||
combine_mat4: expression(numbered("column", 4, "vec4"), { matrix: "mat4" }),
|
||||
separate_u32x16: expression({ value: "u32x16" }, numbered("word", 16, "u32")),
|
||||
combine_u32x16: expression(numbered("word", 16, "u32"), { value: "u32x16" }),
|
||||
separate_u32_bits: expression({ value: "u32" }, numbered("bit", 32, "bool")),
|
||||
combine_u32_bits: expression(numbered("bit", 32, "bool"), { value: "u32" }),
|
||||
separate_local_aabb: expression({ value: "local_aabb" }, { min: "vec3", max: "vec3" }),
|
||||
};
|
||||
const texture = {
|
||||
residency: "transient",
|
||||
texture: {
|
||||
@@ -25,17 +64,13 @@ const texture = {
|
||||
};
|
||||
export const semanticCatalog = Object.freeze({
|
||||
mesh: {
|
||||
version: 1,
|
||||
version: 2,
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: {
|
||||
mesh: o("mesh_data"),
|
||||
localAabbs: o("local_aabb_buffer"),
|
||||
isVisible: {
|
||||
...o("boolean_flag_buffer"),
|
||||
authoringType: "visibility_flag_buffer",
|
||||
},
|
||||
pipelineIndices: o("pipeline_index_stream"),
|
||||
type: o("u32x16"),
|
||||
localAabb: o("local_aabb"),
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
@@ -62,54 +97,28 @@ export const semanticCatalog = Object.freeze({
|
||||
},
|
||||
},
|
||||
frustum_cull: {
|
||||
version: 1,
|
||||
execution: "compute",
|
||||
version: 2,
|
||||
execution: "expression",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
localAabbs: i("local_aabb_buffer"),
|
||||
},
|
||||
outputs: {
|
||||
isFrustumCulled: {
|
||||
...o("boolean_flag_buffer"),
|
||||
authoringType: "frustum_flag_buffer",
|
||||
},
|
||||
localAabb: i("local_aabb"),
|
||||
},
|
||||
outputs: { isFrustumCulled: o("bool") },
|
||||
parameters: { cameraSelection: "active" },
|
||||
},
|
||||
mesh_query: {
|
||||
version: 1,
|
||||
execution: "compute",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
isVisible: i("boolean_flag_buffer", false, "visibility_flag_buffer"),
|
||||
isFrustumCulled: i("boolean_flag_buffer", false, "frustum_flag_buffer"),
|
||||
},
|
||||
outputs: { draws: o("draw_stream") },
|
||||
parameters: {
|
||||
visiblePredicate: "required_true",
|
||||
frustumCulledPredicate: "required_false",
|
||||
},
|
||||
},
|
||||
pipeline_registry: {
|
||||
version: 1,
|
||||
execution: "cpu_preparation",
|
||||
inputs: { pipelineIndices: i("pipeline_index_stream") },
|
||||
outputs: { activation: o("pipeline_activation") },
|
||||
parameters: {},
|
||||
},
|
||||
pipeline: {
|
||||
version: 1,
|
||||
version: 2,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
draws: i("draw_stream"),
|
||||
activation: i("pipeline_activation"),
|
||||
predicate: i("bool", false),
|
||||
colorTarget: i("texture"),
|
||||
depthTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture"), depth: o("texture") },
|
||||
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
|
||||
},
|
||||
...expressionCatalog,
|
||||
fullscreen_copy: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
@@ -210,13 +219,8 @@ export const socketTypes = Object.fromEntries(
|
||||
[
|
||||
"texture",
|
||||
"mesh_data",
|
||||
"local_aabb_buffer",
|
||||
"boolean_flag_buffer",
|
||||
"pipeline_index_stream",
|
||||
"draw_stream",
|
||||
"pipeline_activation",
|
||||
"visibility_flag_buffer",
|
||||
"frustum_flag_buffer",
|
||||
"bool", "f32", "u32", "vec2", "vec3", "vec4",
|
||||
"mat2", "mat3", "mat4", "u32x16", "local_aabb",
|
||||
].map((type, index) => [
|
||||
type,
|
||||
{
|
||||
@@ -226,11 +230,6 @@ export const socketTypes = Object.fromEntries(
|
||||
},
|
||||
]),
|
||||
);
|
||||
socketTypes.boolean_flag_buffer.acceptsFrom = [
|
||||
"boolean_flag_buffer",
|
||||
"visibility_flag_buffer",
|
||||
"frustum_flag_buffer",
|
||||
];
|
||||
export const theme = {
|
||||
background: "#151820",
|
||||
grid: "#292e3a",
|
||||
@@ -266,6 +265,7 @@ export const theme = {
|
||||
export const styles = {
|
||||
source: { header: "#3977a8" },
|
||||
compute: { header: "#725a9b" },
|
||||
expression: { header: "#725a9b" },
|
||||
cpu_preparation: { header: "#8a6d3b" },
|
||||
render: { header: "#426b43" },
|
||||
frame: { header: "#a75d37" },
|
||||
@@ -283,8 +283,8 @@ const tagged = (kind, value) => ({ kind, value: structuredClone(value) });
|
||||
const number = (value, minimum, maximum) => ({
|
||||
type: "number",
|
||||
default: tagged("number", value),
|
||||
minimum,
|
||||
maximum,
|
||||
...(minimum !== undefined ? { minimum } : {}),
|
||||
...(maximum !== undefined ? { maximum } : {}),
|
||||
});
|
||||
const enumeration = (value, values) => ({
|
||||
type: "string",
|
||||
@@ -302,8 +302,38 @@ const color = (value, minimum = 0, maximum = 1) => ({
|
||||
minimum,
|
||||
maximum,
|
||||
});
|
||||
const vector = (value, minimum, maximum) => ({ type: "vector", default: tagged("vector", value), minimum, maximum });
|
||||
const vector = (value, minimum, maximum) => ({
|
||||
type: "vector", default: tagged("vector", value),
|
||||
...(minimum !== undefined ? { minimum } : {}),
|
||||
...(maximum !== undefined ? { maximum } : {}),
|
||||
});
|
||||
const json = (value) => ({ type: "json", default: tagged("json", value) });
|
||||
const socketDefault = (type, value) => {
|
||||
if (type === "bool") return boolean(value);
|
||||
if (type === "f32") return number(value);
|
||||
if (type === "u32") return { ...number(value, 0, 0xffffffff), integer: true };
|
||||
if (type === "vec3") return vector(value);
|
||||
return json(value);
|
||||
};
|
||||
const zero = (type) => {
|
||||
if (type === "bool") return false;
|
||||
if (type === "f32" || type === "u32") return 0;
|
||||
if (/^vec[234]$/.test(type)) return Array(Number(type.at(-1))).fill(0);
|
||||
if (type === "u32x16") return Array(16).fill(0);
|
||||
if (type === "local_aabb") return { min: [0, 0, 0], max: [0, 0, 0] };
|
||||
const size = Number(type.at(-1));
|
||||
return Array.from({ length: size }, (_, column) =>
|
||||
Array.from({ length: size }, (_, row) => Number(column === row)));
|
||||
};
|
||||
const defaultForInput = (key, name, type) => {
|
||||
if (key === "pipeline" && name === "predicate") return true;
|
||||
if (key === "and") return true;
|
||||
if (/^combine_mat[234]$/.test(key)) {
|
||||
const index = Number(name.replace("column", ""));
|
||||
return zero(type).map((_, row) => Number(index === row));
|
||||
}
|
||||
return zero(type);
|
||||
};
|
||||
const parameterSchemas = {
|
||||
texture: {
|
||||
residency: enumeration("transient", ["transient", "persistent"]),
|
||||
@@ -343,19 +373,6 @@ const parameterSchemas = {
|
||||
},
|
||||
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", [
|
||||
@@ -399,6 +416,7 @@ const parameterSchemas = {
|
||||
backgroundColor: color([0, 0, 0, 1]),
|
||||
},
|
||||
};
|
||||
for (const key of Object.keys(expressionCatalog)) parameterSchemas[key] = {};
|
||||
export const nodeDefinitions = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||
const sockets = {
|
||||
@@ -409,11 +427,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
n,
|
||||
"input",
|
||||
v.authoringType ?? v.accepted.types[0],
|
||||
key === "mesh_query" && n === "isVisible"
|
||||
? boolean(true)
|
||||
: key === "mesh_query" && n === "isFrustumCulled"
|
||||
? boolean(false)
|
||||
: null,
|
||||
!v.required ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
|
||||
),
|
||||
]),
|
||||
),
|
||||
@@ -458,6 +472,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
];
|
||||
}),
|
||||
);
|
||||
nodeDefinitions.mesh.sockets.localAabb.title = "Local AABB";
|
||||
nodeDefinitions.color_balance.ui = [
|
||||
{ kind: "parameter", parameter: "mode" },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
|
||||
@@ -3,19 +3,8 @@ import { CATALOG_VERSION, GRAPH_ID, fxNodeComposition } from "./catalog.js";
|
||||
import { prepareBrowserHost } from "./browser-host.js";
|
||||
import { createAddNodeMenu } from "./add-node-menu.js";
|
||||
import { createNodeIdAllocator, spawnRequestedNode } from "./node-spawn.js";
|
||||
import { culling } from "./presets.js";
|
||||
|
||||
const spec = [
|
||||
["hdr", "texture", { x: 40, y: 170 }],
|
||||
["depth", "texture", { x: 40, y: 300 }],
|
||||
["mesh", "mesh", { x: 40, y: 470 }],
|
||||
["cull", "frustum_cull", { x: 540, y: 480 }],
|
||||
["query", "mesh_query", { x: 790, y: 330 }],
|
||||
["registry", "pipeline_registry", { x: 790, y: 620 }],
|
||||
["ground", "pipeline", { x: 1040, y: 290 }],
|
||||
["pbr", "pipeline", { x: 1300, y: 290 }],
|
||||
["pbr_double", "pipeline", { x: 1560, y: 290 }],
|
||||
["frame_out", "frame_out", { x: 1820, y: 250 }],
|
||||
];
|
||||
async function seed(root) {
|
||||
await root.setState({
|
||||
graphId: GRAPH_ID,
|
||||
@@ -24,28 +13,11 @@ async function seed(root) {
|
||||
links: [],
|
||||
metadata: {},
|
||||
});
|
||||
for (const [nodeId, nodeType, position] of spec)
|
||||
await root.dispatch({ type: "node.add", nodeId, nodeType, position });
|
||||
const links = [
|
||||
["mesh", "mesh", "cull", "mesh"],
|
||||
["mesh", "localAabbs", "cull", "localAabbs"],
|
||||
["mesh", "mesh", "query", "mesh"],
|
||||
["mesh", "isVisible", "query", "isVisible"],
|
||||
["cull", "isFrustumCulled", "query", "isFrustumCulled"],
|
||||
["mesh", "pipelineIndices", "registry", "pipelineIndices"],
|
||||
...["ground", "pbr", "pbr_double"].flatMap((pipeline) => [
|
||||
["mesh", "mesh", pipeline, "mesh"],
|
||||
["query", "draws", pipeline, "draws"],
|
||||
["registry", "activation", pipeline, "activation"],
|
||||
]),
|
||||
["hdr", "texture", "ground", "colorTarget"],
|
||||
["depth", "texture", "ground", "depthTarget"],
|
||||
["ground", "color", "pbr", "colorTarget"],
|
||||
["ground", "depth", "pbr", "depthTarget"],
|
||||
["pbr", "color", "pbr_double", "colorTarget"],
|
||||
["pbr", "depth", "pbr_double", "depthTarget"],
|
||||
["pbr_double", "color", "frame_out", "color"],
|
||||
];
|
||||
for (const [index, item] of culling.nodes.entries())
|
||||
await root.dispatch({ type: "node.add", nodeId: item.id, nodeType: item.executor.key,
|
||||
position: { x: 40 + (index % 6) * 280, y: 120 + Math.floor(index / 6) * 260 } });
|
||||
const links = culling.nodes.flatMap((item) => Object.entries(item.inputs).map(([socket, from]) =>
|
||||
[from.node, from.socket, item.id, socket]));
|
||||
for (const [a, as, b, bs] of links) {
|
||||
const id = `${a}_${as}_${b}_${bs}`;
|
||||
await root.dispatch({
|
||||
@@ -61,11 +33,43 @@ async function seed(root) {
|
||||
},
|
||||
});
|
||||
}
|
||||
const authored = await root.getState(),
|
||||
depth = authored.nodes.find((node) => node.id === "depth");
|
||||
depth.parameters.format = { kind: "string", value: "depth32_float" };
|
||||
for (const [id, name] of [["ground", "ground_plane"], ["pbr", "gltf_standard"], ["pbr_double", "gltf_standard_double_sided"]])
|
||||
authored.nodes.find((node) => node.id === id).parameters.pipeline = { kind: "string", value: name };
|
||||
const authored = await root.getState();
|
||||
for (const item of culling.nodes) {
|
||||
const target = authored.nodes.find((candidate) => candidate.id === item.id);
|
||||
if (item.executor.key === "texture") {
|
||||
const texture = item.parameters.texture;
|
||||
const relative = texture.extent.kind === "surface_relative";
|
||||
const values = {
|
||||
residency: item.parameters.residency,
|
||||
format: texture.format,
|
||||
dimension: texture.dimension,
|
||||
extentMode: texture.extent.kind,
|
||||
absoluteWidth: relative ? 1 : texture.extent.width,
|
||||
absoluteHeight: relative ? 1 : texture.extent.height,
|
||||
relativeWidthNumerator: relative ? texture.extent.width.numerator : 1,
|
||||
relativeWidthDenominator: relative ? texture.extent.width.denominator : 1,
|
||||
relativeHeightNumerator: relative ? texture.extent.height.numerator : 1,
|
||||
relativeHeightDenominator: relative ? texture.extent.height.denominator : 1,
|
||||
depthOrArrayLayers: texture.extent.depthOrArrayLayers,
|
||||
mipLevelCount: texture.mipLevelCount,
|
||||
sampleCount: String(texture.sampleCount),
|
||||
viewFormat: texture.viewFormats[0] ?? "none",
|
||||
};
|
||||
for (const [key, value] of Object.entries(values))
|
||||
target.parameters[key].value = structuredClone(value);
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(item.parameters)) {
|
||||
const input = key.endsWith("Default") ? key.slice(0, -7) : null;
|
||||
if (input) {
|
||||
const socket = target.sockets.find((candidate) => candidate.key === input);
|
||||
if (socket?.defaultValue) socket.defaultValue.value = structuredClone(value);
|
||||
} else {
|
||||
const authoredKey = item.executor.key === "frustum_cull" && key === "camera" ? "cameraSelection" : key;
|
||||
if (target.parameters[authoredKey]) target.parameters[authoredKey].value = structuredClone(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
await root.setState(authored);
|
||||
}
|
||||
export async function createRenderGraphEditor(canvas) {
|
||||
|
||||
@@ -13,16 +13,37 @@ const texture = (format, scale = 1, heightScale = scale) => ({
|
||||
residency: "transient",
|
||||
});
|
||||
const frameOut = (hdr, options = {}) => ({ surfaceFormat: "preferred", hdrEnabled: hdr, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1], ...options });
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1) => [
|
||||
const predicates = (withCulling = false) => {
|
||||
const result = [
|
||||
node("type_words", "separate_u32x16", { valueDefault: Array(16).fill(0) }, { value: input("mesh", "type") }),
|
||||
node("type_bits", "separate_u32_bits", { valueDefault: 0 }, { value: input("type_words", "word0") }),
|
||||
node("ground_class", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit1") }),
|
||||
node("visible_pbr", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit2") }),
|
||||
node("not_double", "not", { operandDefault: false }, { operand: input("type_bits", "bit3") }),
|
||||
node("standard_class", "and", { leftDefault: true, rightDefault: true }, { left: input("visible_pbr", "value"), right: input("not_double", "value") }),
|
||||
node("double_class", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit3") }),
|
||||
];
|
||||
if (!withCulling) return { nodes: result, classes: { ground: "ground_class", pbr: "standard_class", pbr_double: "double_class" } };
|
||||
result.push(
|
||||
node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabb: input("mesh", "localAabb") }),
|
||||
node("not_culled", "not", { operandDefault: false }, { operand: input("cull", "isFrustumCulled") }),
|
||||
...[["ground", "ground_class"], ["pbr", "standard_class"], ["pbr_double", "double_class"]].map(([name, classification]) =>
|
||||
node(`${name}_final`, "and", { leftDefault: true, rightDefault: true }, { left: input(classification, "value"), right: input("not_culled", "value") })),
|
||||
);
|
||||
return { nodes: result, classes: { ground: "ground_final", pbr: "pbr_final", pbr_double: "pbr_double_final" } };
|
||||
};
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false) => {
|
||||
const classification = predicates(withCulling);
|
||||
return [
|
||||
node("hdr", "texture", texture("rgba16_float", 1, heightScale)),
|
||||
node("depth", "texture", texture("depth32_float", 1, heightScale)),
|
||||
node("mesh", "mesh"),
|
||||
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
|
||||
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
|
||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
|
||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
||||
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||
...classification.nodes,
|
||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
|
||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
||||
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||
];
|
||||
};
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
||||
const direct = (graphId, clearColor) => graph(graphId, [
|
||||
node("ldr", "texture", texture("rgba8_unorm")),
|
||||
@@ -36,12 +57,7 @@ export const hdr = graph("preset_hdr_fullscreen", [
|
||||
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
||||
]);
|
||||
export const culling = graph("preset_gpu_culling", (() => {
|
||||
const nodes = structuredClone(hdr.nodes);
|
||||
nodes.splice(3, 0, node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabbs: input("mesh", "localAabbs") }));
|
||||
const query = nodes.find((item) => item.id === "query");
|
||||
query.parameters.frustumCulledPredicate = "required_false";
|
||||
query.inputs.isFrustumCulled = input("cull", "isFrustumCulled");
|
||||
return nodes;
|
||||
return [...scene("hdr", undefined, 1, true), node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") })];
|
||||
})());
|
||||
const postPreset = (graphId, kind) => {
|
||||
const nodes = [...scene("hdr")];
|
||||
|
||||
+21
-16
@@ -1,8 +1,8 @@
|
||||
import { SnapshotReader } from "./render-data-snapshot.js";
|
||||
|
||||
export const VISIBLE = 1;
|
||||
const HEADER_WORDS = 16, SLOT_WORDS = 24, CAPACITY = 1024, SLOT_VERSION = 1;
|
||||
const OP = { IMPORT_GLB: 1, MESH_FLAGS: 2, CREATE_INSTANCE: 3, INSTANCE_FLAGS: 4, INSTANCE_TRANSFORM: 5, DESTROY_INSTANCE: 6, COMPILE_GRAPH: 7, DROP_GRAPH: 8, SWITCH_GRAPH: 9 };
|
||||
const HEADER_WORDS = 16, SLOT_WORDS = 40, CAPACITY = 1024, SLOT_VERSION = 2;
|
||||
const OP = { IMPORT_GLB: 1, MESH_FLAGS: 2, CREATE_INSTANCE: 3, INSTANCE_VISIBLE: 4, INSTANCE_TRANSFORM: 5, DESTROY_INSTANCE: 6, COMPILE_GRAPH: 7, DROP_GRAPH: 8, SWITCH_GRAPH: 9, SET_INSTANCE_TYPE: 10 };
|
||||
const HANDLE_TOKEN = Symbol("renderer handle");
|
||||
|
||||
export class RendererError extends Error {
|
||||
@@ -20,7 +20,7 @@ export class RendererClient {
|
||||
this.#bridge = bridge;
|
||||
this.#worker = bridge.worker;
|
||||
this.#refreshViews();
|
||||
if (Atomics.load(this.#header, 0) !== 0x4e574159 || Atomics.load(this.#header, 1) !== 1 || Atomics.load(this.#header, 2) !== CAPACITY || Atomics.load(this.#header, 3) !== SLOT_WORDS) {
|
||||
if (Atomics.load(this.#header, 0) !== 0x4e574159 || Atomics.load(this.#header, 1) !== 2 || Atomics.load(this.#header, 2) !== CAPACITY || Atomics.load(this.#header, 3) !== SLOT_WORDS) {
|
||||
try { this.#worker?.terminate?.(); } catch { /* best effort */ }
|
||||
try { bridge?.free?.(); } catch { /* best effort */ }
|
||||
throw new RendererError("PROTOCOL_MISMATCH");
|
||||
@@ -70,9 +70,9 @@ export class RendererClient {
|
||||
this.#fail(message.code || "WORKER_FATAL");
|
||||
} else if (message?.type === "snapshot-init") {
|
||||
try {
|
||||
if (message.controlVersion !== 1 || message.schemaVersion !== 1) throw new Error("version");
|
||||
if (message.controlVersion !== 1 || message.schemaVersion !== 2) throw new Error("version");
|
||||
this.#snapshotReader = new SnapshotReader(this.#bridge.memory, message.controlPtr);
|
||||
this.#bvh?.postMessage({type:"init",memory:this.#bridge.memory,controlPtr:message.controlPtr,controlVersion:1,schemaVersion:1});
|
||||
this.#bvh?.postMessage({type:"init",memory:this.#bridge.memory,controlPtr:message.controlPtr,controlVersion:1,schemaVersion:2});
|
||||
} catch { this.#disablePicking("PICK_PROTOCOL_MISMATCH"); }
|
||||
} else if (message?.type === "snapshot-published") {
|
||||
try { this.#snapshotEpoch=this.#snapshotReader?.latest().epoch||0; this.#bvh?.postMessage({type:"update",epoch:this.#snapshotEpoch}); } catch { this.#disablePicking("PICK_PROTOCOL_MISMATCH"); }
|
||||
@@ -157,18 +157,20 @@ export class RendererClient {
|
||||
return promise;
|
||||
}
|
||||
|
||||
#mesh(handle) {
|
||||
#mesh(handle, defaultType = Array(16).fill(0)) {
|
||||
return new Mesh(HANDLE_TOKEN,
|
||||
visible => this.#enqueue(OP.MESH_FLAGS, [...handle, visible ? VISIBLE : 0]),
|
||||
async (transform, visible) => {
|
||||
const result = await this.#enqueue(OP.CREATE_INSTANCE, [...handle, ...floatWords(transform), visible ? VISIBLE : 0]);
|
||||
visible => { validateVisible(visible); return this.#enqueue(OP.MESH_FLAGS, [...handle, visible ? 1 : 0]); },
|
||||
async (transform, {type = defaultType, visible} = {}) => {
|
||||
type = typeWords(type); if (visible !== undefined) { validateVisible(visible); type[0] = (type[0] & ~1) | Number(visible); }
|
||||
const result = await this.#enqueue(OP.CREATE_INSTANCE, [...handle, ...floatWords(transform), ...type]);
|
||||
return this.#instance(result);
|
||||
});
|
||||
}
|
||||
|
||||
#instance(handle) {
|
||||
return new Instance(HANDLE_TOKEN,
|
||||
visible => this.#enqueue(OP.INSTANCE_FLAGS, [...handle, visible ? VISIBLE : 0]),
|
||||
visible => { validateVisible(visible); return this.#enqueue(OP.INSTANCE_VISIBLE, [...handle, visible ? 1 : 0]); },
|
||||
type => this.#enqueue(OP.SET_INSTANCE_TYPE, [...handle, ...typeWords(type)]),
|
||||
transform => this.#enqueue(OP.INSTANCE_TRANSFORM, [...handle, ...floatWords(transform)]),
|
||||
() => this.#enqueue(OP.DESTROY_INSTANCE, [...handle]));
|
||||
}
|
||||
@@ -183,11 +185,9 @@ export class RendererClient {
|
||||
else throw new TypeError("GLB source must be URL, File, or ArrayBuffer");
|
||||
if (this.#disposed) throw new RendererError("DISPOSED");
|
||||
const result = await this.#withPayload(buffer, OP.IMPORT_GLB, [framing === "interior" ? 1 : 0]);
|
||||
return result.meshes.map(handle => this.#mesh(handle));
|
||||
return result.meshes.map(item => this.#mesh(item.handle, item.defaultType));
|
||||
}
|
||||
|
||||
/** Compatibility alias for the original opcode-1 API. */
|
||||
importGlb(source, options) { return this.replaceSceneGlb(source, options); }
|
||||
|
||||
async #withPayload(buffer, opcode, words = []) {
|
||||
if (this.#disposed) throw new RendererError("DISPOSED");
|
||||
@@ -260,6 +260,9 @@ function validateCompiledId(compiledId) {
|
||||
if (!Array.isArray(compiledId) || compiledId.length !== 2 || compiledId.some(word => !Number.isInteger(word) || word < 0 || word > 0xffffffff)) throw new TypeError("compiledId must contain exactly two uint32 values");
|
||||
}
|
||||
|
||||
function validateVisible(value) { if (typeof value !== "boolean") throw new TypeError("visible must be boolean"); }
|
||||
function typeWords(words) { if (!words || words.length !== 16 || [...words].some(x => !Number.isInteger(x) || x < 0 || x > 0xffffffff)) throw new TypeError("type must contain exactly 16 uint32 values"); return Array.from(words, x => x >>> 0); }
|
||||
|
||||
function floatWords(matrix) {
|
||||
if (!matrix || matrix.length !== 16) throw new TypeError("transform must contain 16 numbers");
|
||||
return [...new Int32Array(new Float32Array(matrix).buffer)];
|
||||
@@ -273,19 +276,21 @@ class Mesh {
|
||||
this.#createInstance = createInstance;
|
||||
}
|
||||
setVisible(visible) { return this.#setVisible(visible); }
|
||||
createInstance(transform, visible = true) { return this.#createInstance(transform, visible); }
|
||||
createInstance(transform, options = {}) { return this.#createInstance(transform, options); }
|
||||
}
|
||||
|
||||
class Instance {
|
||||
#setVisible; #setTransform; #destroy; #dead = false;
|
||||
constructor(token, setVisible, setTransform, destroy) {
|
||||
#setVisible; #setType; #setTransform; #destroy; #dead = false;
|
||||
constructor(token, setVisible, setType, setTransform, destroy) {
|
||||
if (token !== HANDLE_TOKEN) throw new TypeError("Instance cannot be constructed directly");
|
||||
this.#setVisible = setVisible;
|
||||
this.#setType = setType;
|
||||
this.#setTransform = setTransform;
|
||||
this.#destroy = destroy;
|
||||
}
|
||||
#live() { if (this.#dead) throw new RendererError("STALE_HANDLE"); }
|
||||
setVisible(visible) { this.#live(); return this.#setVisible(visible); }
|
||||
setType(words) { this.#live(); return this.#setType(words); }
|
||||
setTransform(transform) { this.#live(); return this.#setTransform(transform); }
|
||||
async destroy() { this.#live(); await this.#destroy(); this.#dead = true; }
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ import {
|
||||
spawnRequestedNode,
|
||||
} from "../static/render-graph/node-spawn.js";
|
||||
|
||||
test("add-node model contains all 16 catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 16);
|
||||
test("add-node model contains all final catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 42);
|
||||
assert.deepEqual(
|
||||
[...new Set(addNodeItems.map((item) => item.group))],
|
||||
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
||||
["Source", "Expression", "Render / post", "Frame"],
|
||||
);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 16);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 42);
|
||||
assert.ok(addNodeItems.some((item) => item.typeId === "separate_u32_bits" && item.group === "Expression"));
|
||||
assert.ok(!addNodeItems.some((item) => ["mesh_query", "pipeline_registry"].includes(item.typeId)));
|
||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
||||
});
|
||||
|
||||
@@ -36,7 +38,7 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("all 16 types spawn with exact position, current version and generated ID", async () => {
|
||||
test("all final types spawn with exact position, current version and generated ID", async () => {
|
||||
let revision = 5,
|
||||
expectedType;
|
||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
||||
|
||||
@@ -36,13 +36,30 @@ test("production render graph composition passes fxnode's public validator", asy
|
||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||
);
|
||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||
assert.equal(fxNodeComposition.version, 7);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 16);
|
||||
assert.equal(fxNodeComposition.version, 8);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 42);
|
||||
assert.ok(
|
||||
Object.values(fxNodeComposition.nodes).every(
|
||||
(definition) => definition.migrations.length === 0,
|
||||
),
|
||||
);
|
||||
for (const [type, descriptor] of Object.entries(fxNodeComposition.nodes)) {
|
||||
const socketKeys = Object.keys(descriptor.sockets);
|
||||
assert.equal(
|
||||
socketKeys.length,
|
||||
new Set(socketKeys).size,
|
||||
`${type} has colliding input and output socket names`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(fxNodeComposition.nodes.not.sockets.operand, {
|
||||
title: "operand",
|
||||
direction: "input",
|
||||
type: "bool",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: { type: "boolean", default: { kind: "boolean", value: false } },
|
||||
showValue: true,
|
||||
});
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,660 +1,40 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
adaptFxNodeSnapshot,
|
||||
AuthoringGraphError,
|
||||
getSourceMap,
|
||||
mapAuthoringDiagnostic,
|
||||
} from "../static/render-graph/adapter.js";
|
||||
import { RendererError } from "../static/renderer-client.js";
|
||||
import {
|
||||
semanticCatalog,
|
||||
nodeDefinitions,
|
||||
GRAPH_ID,
|
||||
CATALOG_VERSION,
|
||||
descriptors,
|
||||
socketTypes,
|
||||
} from "../static/render-graph/catalog.js";
|
||||
import { culling } from "../static/render-graph/presets.js";
|
||||
import { AuthoringController } from "../static/render-graph/authoring-controller.js";
|
||||
function fixture() {
|
||||
const nodes = culling.nodes.map((n) => {
|
||||
const d = semanticCatalog[n.executor.key];
|
||||
const definition = nodeDefinitions[n.executor.key];
|
||||
return {
|
||||
id: n.id,
|
||||
typeId: n.executor.key,
|
||||
typeVersion: d.version,
|
||||
known: true,
|
||||
muted: n.state !== "enabled",
|
||||
position: { x: 10, y: 20 },
|
||||
size: { x: 200, y: 120 },
|
||||
label: n.id,
|
||||
collapsed: false,
|
||||
extensions: {},
|
||||
parameters: Object.fromEntries(
|
||||
Object.entries(definition.parameters).map(([key, schema]) => [
|
||||
key,
|
||||
{
|
||||
kind: schema.type,
|
||||
value: structuredClone(n.parameters[key] ?? schema.default.value),
|
||||
},
|
||||
]),
|
||||
),
|
||||
sockets: [
|
||||
...Object.entries(d.inputs).map(([key, x]) => {
|
||||
const socket = definition.sockets[key];
|
||||
return {
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
direction: "input",
|
||||
dataType: x.authoringType ?? x.accepted.types[0],
|
||||
label: socket.title,
|
||||
accepts:
|
||||
socketTypes[x.authoringType ?? x.accepted.types[0]].acceptsFrom,
|
||||
...(socket.value
|
||||
? { defaultValue: structuredClone(socket.value.default) }
|
||||
: {}),
|
||||
visible: socket.visible,
|
||||
maxIncomingLinks: socket.maxIncomingLinks,
|
||||
};
|
||||
}),
|
||||
...Object.entries(d.outputs).map(([key]) => ({
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
direction: "output",
|
||||
dataType: definition.sockets[key].type,
|
||||
label: key,
|
||||
accepts: [],
|
||||
visible: true,
|
||||
maxIncomingLinks: 0,
|
||||
})),
|
||||
],
|
||||
};
|
||||
});
|
||||
const links = [];
|
||||
for (const n of culling.nodes)
|
||||
for (const [socket, from] of Object.entries(n.inputs))
|
||||
links.push({
|
||||
id: `l_${from.node}_${from.socket}_${n.id}_${socket}`,
|
||||
fromNodeId: from.node,
|
||||
fromSocketId: `${from.node}:${from.socket}`,
|
||||
toNodeId: n.id,
|
||||
toSocketId: `${n.id}:${socket}`,
|
||||
muted: false,
|
||||
extensions: {},
|
||||
});
|
||||
return {
|
||||
graphId: GRAPH_ID,
|
||||
catalogVersion: CATALOG_VERSION,
|
||||
nodes,
|
||||
links,
|
||||
metadata: { layout: "ignored" },
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
test("catalog exhaustively mirrors all current contracts", () => {
|
||||
for (const [key, semantic] of Object.entries(semanticCatalog)) {
|
||||
assert.ok(Object.hasOwn(semantic, "version"));
|
||||
assert.equal(semantic.version, key === "frame_out" ? 3 : 1);
|
||||
assert.equal(nodeDefinitions[key].version, semantic.version);
|
||||
assert.equal(descriptors[key].version, semantic.version);
|
||||
}
|
||||
assert.deepEqual(
|
||||
Object.keys(semanticCatalog),
|
||||
[
|
||||
"mesh",
|
||||
"texture",
|
||||
"frustum_cull",
|
||||
"mesh_query",
|
||||
"pipeline_registry",
|
||||
"pipeline",
|
||||
"fullscreen_copy",
|
||||
"color_balance",
|
||||
"exposure_contrast",
|
||||
"saturation",
|
||||
"channel_mixer",
|
||||
"bloom_extract",
|
||||
"bloom_blur",
|
||||
"bloom_composite",
|
||||
"luminance_edge",
|
||||
"frame_out",
|
||||
],
|
||||
);
|
||||
for (const c of Object.values(semanticCatalog)) {
|
||||
assert.ok(c.execution);
|
||||
assert.ok(c.inputs);
|
||||
assert.ok(c.outputs);
|
||||
assert.ok(c.parameters);
|
||||
}
|
||||
for (const [key, contract] of Object.entries(semanticCatalog))
|
||||
assert.deepEqual(
|
||||
Object.keys(nodeDefinitions[key].parameters).sort(),
|
||||
Object.keys(contract.parameters).sort(),
|
||||
key,
|
||||
);
|
||||
assert.equal(CATALOG_VERSION, 7);
|
||||
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);
|
||||
import {
|
||||
CATALOG_VERSION, semanticCatalog, nodeDefinitions, descriptors,
|
||||
} from "../static/render-graph/catalog.js";
|
||||
|
||||
assert.deepEqual(nodeDefinitions.color_balance.ui.slice(0, 4), [
|
||||
{ kind: "parameter", parameter: "mode" },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
{ title: "Lift", scalar: "lift", color: "liftColor" },
|
||||
{ title: "Gamma", scalar: "gamma", color: "gammaColor" },
|
||||
{ title: "Gain", scalar: "gain", color: "gainColor" },
|
||||
], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
{ title: "Offset", scalar: "offset", color: "offsetColor" },
|
||||
{ title: "Power", scalar: "power", color: "powerColor" },
|
||||
{ title: "Slope", scalar: "slope", color: "slopeColor" },
|
||||
], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } },
|
||||
{ kind: "parameter", parameter: "factor" },
|
||||
]);
|
||||
for (const name of ["liftColor", "gammaColor", "gainColor", "offsetColor", "powerColor", "slopeColor"])
|
||||
assert.deepEqual(nodeDefinitions.color_balance.parameters[name].default, { kind: "color", value: [1, 1, 1, 1] });
|
||||
assert.deepEqual(nodeDefinitions.channel_mixer.parameters.redOutput, {
|
||||
type: "vector", default: { kind: "vector", value: [1, 0, 0] }, minimum: -2, maximum: 2,
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
}),
|
||||
);
|
||||
blur.typeVersion = descriptors.bloom_blur.version;
|
||||
x.nodes.push(blur);
|
||||
assert.deepEqual(
|
||||
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "blur").parameters
|
||||
.direction,
|
||||
[0, 1],
|
||||
);
|
||||
pipeline.parameters.clearColor.value[0] = 2;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
pipeline.parameters.clearColor.value = [0, 0, 0];
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
pipeline.parameters.clearColor.value = [0, 0, Number.NaN, 1];
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
const texture = x.nodes.find((node) => node.id === "hdr");
|
||||
texture.parameters.residency.value = "unknown";
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
texture.parameters.residency.value = "transient";
|
||||
pipeline.parameters.clearDepth.value = -1;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
});
|
||||
|
||||
test("adapter validates and lowers disconnected query socket defaults", () => {
|
||||
const x = fixture();
|
||||
const query = x.nodes.find((node) => node.id === "query");
|
||||
const visible = query.sockets.find((socket) => socket.key === "isVisible");
|
||||
visible.defaultValue.value = false;
|
||||
const ir = adaptFxNodeSnapshot(x);
|
||||
assert.equal(visible.defaultValue.value, false);
|
||||
const parameters = ir.nodes.find((node) => node.id === "query").parameters;
|
||||
assert.equal(parameters.isVisible, undefined);
|
||||
assert.equal(parameters.visibleDefault, false);
|
||||
assert.equal(parameters.frustumCulledDefault, false);
|
||||
visible.defaultValue = { kind: "number", value: 0 };
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_SOCKET",
|
||||
);
|
||||
delete visible.defaultValue;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_SOCKET",
|
||||
);
|
||||
});
|
||||
test("adapter lowers the authoring-safe camera selector to the Rust wire field", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const parameters = ir.nodes.find((node) => node.id === "cull").parameters;
|
||||
assert.deepEqual(parameters, { camera: "active" });
|
||||
assert.equal(parameters.cameraSelection, undefined);
|
||||
});
|
||||
test("adapter deterministically emits the canonical schema, permits repeated types, omits muted links and maps sources", () => {
|
||||
const x = fixture(),
|
||||
a = adaptFxNodeSnapshot(x, 7);
|
||||
x.nodes.reverse();
|
||||
x.links.reverse();
|
||||
assert.deepEqual(adaptFxNodeSnapshot(x, 7), a);
|
||||
assert.equal(a.schemaVersion, 2);
|
||||
assert.equal(a.graphId, GRAPH_ID);
|
||||
assert.equal(a.nodes.filter((n) => n.executor.key === "texture").length, 2);
|
||||
assert.ok(
|
||||
Object.values(getSourceMap(a)).some((source) => source.input === "color"),
|
||||
);
|
||||
x.links.find((l) => l.id === "l_pbr_double_color_frame_out_color").muted = true;
|
||||
assert.equal(
|
||||
adaptFxNodeSnapshot(x, 8).nodes.find((n) => n.id === "frame_out").inputs
|
||||
.color,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
test("adapter rejects hostile shape, IDs, duplicates, catalog, sockets and type mismatches", () => {
|
||||
const reject = (fn, code) => {
|
||||
const x = fixture();
|
||||
fn(x);
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(e) => e instanceof AuthoringGraphError && e.code === code,
|
||||
);
|
||||
};
|
||||
reject((x) => (x.graphId = "bad"), "AUTHORING_CATALOG");
|
||||
reject((x) => (x.catalogVersion = 2), "AUTHORING_CATALOG");
|
||||
reject((x) => (x.nodes[0].id = "bad id"), "AUTHORING_ID");
|
||||
reject((x) => (x.nodes[1].id = x.nodes[0].id), "AUTHORING_ID_DUPLICATE");
|
||||
reject((x) => (x.nodes[0].typeId = "wat"), "AUTHORING_NODE_TYPE");
|
||||
reject((x) => (x.nodes[0].typeVersion = 2), "AUTHORING_NODE_INVALID");
|
||||
reject((x) => (x.nodes[0].sockets = []), "AUTHORING_SOCKET_SET");
|
||||
reject((x) => (x.links[0].toSocketId = "missing:x"), "AUTHORING_LINK");
|
||||
reject((x) => {
|
||||
const link = x.links.find((l) => l.toSocketId === "frame_out:color");
|
||||
link.fromNodeId = "mesh";
|
||||
link.fromSocketId = "mesh:mesh";
|
||||
}, "AUTHORING_LINK_TYPE");
|
||||
});
|
||||
test("Frame Out has the exact v3 schema, defaults, UI, and strict authoring validation", () => {
|
||||
const fields = ["surfaceFormat", "hdrEnabled", "toneMapper", "exposureStops", "outputTransfer", "scaleMode", "filter", "backgroundColor"];
|
||||
assert.equal(CATALOG_VERSION, 7);
|
||||
assert.deepEqual(semanticCatalog.frame_out, {
|
||||
version: 3, execution: "frame", inputs: { color: semanticCatalog.frame_out.inputs.color }, outputs: {},
|
||||
parameters: { surfaceFormat: "preferred", hdrEnabled: true, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1] },
|
||||
});
|
||||
assert.deepEqual(nodeDefinitions.frame_out.parameters, {
|
||||
surfaceFormat: { type: "string", default: { kind: "string", value: "preferred" }, enum: ["preferred", "rgba8_unorm", "bgra8_unorm", "rgba16_float"] },
|
||||
hdrEnabled: { type: "boolean", default: { kind: "boolean", value: true } },
|
||||
toneMapper: { type: "string", default: { kind: "string", value: "aces" }, enum: ["aces", "reinhard", "none"] },
|
||||
exposureStops: { type: "number", default: { kind: "number", value: 0 }, minimum: -10, maximum: 10 },
|
||||
outputTransfer: { type: "string", default: { kind: "string", value: "srgb" }, enum: ["srgb", "linear"] },
|
||||
scaleMode: { type: "string", default: { kind: "string", value: "stretch" }, enum: ["stretch", "contain", "cover"] },
|
||||
filter: { type: "string", default: { kind: "string", value: "linear" }, enum: ["linear", "nearest"] },
|
||||
backgroundColor: { type: "color", default: { kind: "color", value: [0, 0, 0, 1] }, minimum: 0, maximum: 1 },
|
||||
});
|
||||
assert.deepEqual(nodeDefinitions.frame_out.ui, [
|
||||
{ kind: "text", variant: "section", title: "Canvas Presentation" },
|
||||
{ kind: "parameter", parameter: "surfaceFormat", title: "Surface Format" },
|
||||
{ kind: "text", variant: "section", title: "Display Transform" },
|
||||
{ kind: "parameter", parameter: "hdrEnabled", title: "HDR" },
|
||||
{ kind: "parameter", parameter: "toneMapper", title: "Tone Mapper", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||
{ kind: "parameter", parameter: "exposureStops", title: "Exposure", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||
{ kind: "parameter", parameter: "outputTransfer", title: "Transfer" },
|
||||
{ kind: "parameter", parameter: "scaleMode", title: "Scale" },
|
||||
{ kind: "parameter", parameter: "filter" },
|
||||
{ kind: "parameter", parameter: "backgroundColor", title: "Background", visibleWhen: { parameter: "scaleMode", equals: "contain" } },
|
||||
{ kind: "socket", socket: "color" },
|
||||
]);
|
||||
const reject = (mutate, code, parameter) => {
|
||||
const x = fixture(), n = x.nodes.find((node) => node.typeId === "frame_out");
|
||||
mutate(x, n);
|
||||
assert.throws(() => adaptFxNodeSnapshot(x), (e) => e instanceof AuthoringGraphError && e.code === code && (!parameter || e.details.nodeId === n.id && e.details.parameter === parameter));
|
||||
};
|
||||
reject((x) => x.catalogVersion = 5, "AUTHORING_CATALOG");
|
||||
reject((x, n) => n.typeVersion = 2, "AUTHORING_NODE_INVALID");
|
||||
for (const field of fields) reject((x, n) => delete n.parameters[field], "AUTHORING_PARAMETER_SET");
|
||||
reject((x, n) => n.parameters.extra = { kind: "number", value: 0 }, "AUTHORING_PARAMETER_SET");
|
||||
for (const [field, value] of [
|
||||
["surfaceFormat", "bad"], ["hdrEnabled", 1], ["toneMapper", "bad"], ["outputTransfer", "bad"], ["scaleMode", "bad"], ["filter", "bad"],
|
||||
["exposureStops", NaN], ["exposureStops", -10.01], ["exposureStops", 10.01],
|
||||
["backgroundColor", [0, 0, 0]], ["backgroundColor", [0, 0, Infinity, 1]], ["backgroundColor", [-0.01, 0, 0, 1]], ["backgroundColor", [0, 0, 0, 1.01]],
|
||||
]) reject((x, n) => n.parameters[field].value = value, "AUTHORING_PARAMETER", field);
|
||||
for (const [hidden, value] of [["toneMapper", "bad"], ["exposureStops", Infinity]])
|
||||
reject((x, n) => { n.parameters.hdrEnabled.value = false; n.parameters[hidden].value = value; }, "AUTHORING_PARAMETER", hidden);
|
||||
reject((x, n) => { n.parameters.scaleMode.value = "stretch"; n.parameters.backgroundColor.value = [2, 0, 0, 1]; }, "AUTHORING_PARAMETER", "backgroundColor");
|
||||
});
|
||||
test("adapter counts only active incoming links and reports socket overflow", () => {
|
||||
const x = fixture();
|
||||
const active = x.links.find((link) => link.toSocketId === "frame_out:color");
|
||||
x.links.push({
|
||||
...structuredClone(active),
|
||||
id: "muted_duplicate",
|
||||
muted: true,
|
||||
});
|
||||
assert.doesNotThrow(() => adaptFxNodeSnapshot(x));
|
||||
x.links.push({ ...structuredClone(active), id: "active_overflow" });
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) =>
|
||||
error.code === "AUTHORING_LINK_INCOMING" &&
|
||||
error.details.socketId === "frame_out:color",
|
||||
);
|
||||
});
|
||||
test("source map covers Rust fields, nested values, every input and is deeply frozen", () => {
|
||||
const snapshot = fixture();
|
||||
snapshot.links.find((link) => link.toSocketId === "frame_out:color").muted =
|
||||
true;
|
||||
const ir = adaptFxNodeSnapshot(snapshot, 9),
|
||||
map = getSourceMap(ir);
|
||||
for (const path of [
|
||||
"schemaVersion",
|
||||
"graphId",
|
||||
"revision",
|
||||
"nodes",
|
||||
"nodes[0].id",
|
||||
"nodes[0].state",
|
||||
"nodes[0].executor.key",
|
||||
"nodes[0].executor.version",
|
||||
"nodes[0].parameters",
|
||||
"nodes[0].inputs",
|
||||
])
|
||||
assert.ok(map[path], path);
|
||||
for (const [index, node] of ir.nodes.entries())
|
||||
for (const input of Object.keys(semanticCatalog[node.executor.key].inputs))
|
||||
assert.ok(map[`nodes[${index}].inputs.${input}`]);
|
||||
assert.ok(
|
||||
Object.keys(map).some((path) =>
|
||||
/parameters\..+\[|parameters\..+\..+/.test(path),
|
||||
),
|
||||
);
|
||||
const socket = Object.values(map).find((source) => source.kind === "socket");
|
||||
const unconnected = Object.values(map).find(
|
||||
(source) => source.unconnected === true,
|
||||
);
|
||||
const link = Object.values(map).find((source) => source.kind === "link");
|
||||
assert.ok(socket?.socketId && unconnected?.socketId);
|
||||
assert.equal(
|
||||
ir.nodes.find((node) => node.id === "frame_out").inputs.source,
|
||||
undefined,
|
||||
);
|
||||
for (const field of [
|
||||
"linkId",
|
||||
"fromNodeId",
|
||||
"fromSocketId",
|
||||
"toNodeId",
|
||||
"toSocketId",
|
||||
"muted",
|
||||
])
|
||||
assert.ok(Object.hasOwn(link, field), field);
|
||||
assert.ok(Object.isFrozen(map) && Object.isFrozen(link));
|
||||
});
|
||||
test("texture source maps identify every flat authored control", () => {
|
||||
const snapshot = fixture();
|
||||
const hdr = snapshot.nodes.find((node) => node.id === "hdr");
|
||||
hdr.parameters.viewFormat.value = "rgba16_float";
|
||||
const ir = adaptFxNodeSnapshot(snapshot);
|
||||
const index = ir.nodes.findIndex((node) => node.id === "hdr");
|
||||
const root = `nodes[${index}].parameters`;
|
||||
const map = getSourceMap(ir);
|
||||
const source = (parameter) => ({
|
||||
kind: "parameter",
|
||||
nodeId: "hdr",
|
||||
parameter,
|
||||
});
|
||||
assert.deepEqual(map[`${root}.residency`], source("residency"));
|
||||
assert.deepEqual(map[`${root}.texture`], { kind: "node", nodeId: "hdr" });
|
||||
for (const [path, parameter] of [
|
||||
["dimension", "dimension"],
|
||||
["format", "format"],
|
||||
["extent", "extentMode"],
|
||||
["extent.kind", "extentMode"],
|
||||
["extent.depthOrArrayLayers", "depthOrArrayLayers"],
|
||||
["extent.width", "extentMode"],
|
||||
["extent.width.numerator", "relativeWidthNumerator"],
|
||||
["extent.width.denominator", "relativeWidthDenominator"],
|
||||
["extent.height", "extentMode"],
|
||||
["extent.height.numerator", "relativeHeightNumerator"],
|
||||
["extent.height.denominator", "relativeHeightDenominator"],
|
||||
["mipLevelCount", "mipLevelCount"],
|
||||
["sampleCount", "sampleCount"],
|
||||
["viewFormats", "viewFormat"],
|
||||
["viewFormats[0]", "viewFormat"],
|
||||
])
|
||||
assert.deepEqual(map[`${root}.texture.${path}`], source(parameter), path);
|
||||
assert.ok(!Object.values(map).some((value) => value.parameter === "texture"));
|
||||
|
||||
const absolute = fixture();
|
||||
absolute.nodes.find((node) => node.id === "hdr").parameters.extentMode.value =
|
||||
"absolute";
|
||||
const absoluteIr = adaptFxNodeSnapshot(absolute);
|
||||
const absoluteIndex = absoluteIr.nodes.findIndex((node) => node.id === "hdr");
|
||||
const absoluteMap = getSourceMap(absoluteIr);
|
||||
assert.deepEqual(
|
||||
absoluteMap[`nodes[${absoluteIndex}].parameters.texture.extent.width`],
|
||||
source("absoluteWidth"),
|
||||
);
|
||||
assert.deepEqual(
|
||||
absoluteMap[`nodes[${absoluteIndex}].parameters.texture.extent.height`],
|
||||
source("absoluteHeight"),
|
||||
);
|
||||
});
|
||||
test("unsupported texture diagnostics map to their exact authored controls", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const index = ir.nodes.findIndex((node) => node.id === "hdr");
|
||||
for (const [suffix, parameter] of [
|
||||
["dimension", "dimension"],
|
||||
["mipLevelCount", "mipLevelCount"],
|
||||
["sampleCount", "sampleCount"],
|
||||
["extent.depthOrArrayLayers", "depthOrArrayLayers"],
|
||||
]) {
|
||||
const path = `nodes[${index}].parameters.texture.${suffix}`;
|
||||
const mapped = mapAuthoringDiagnostic(
|
||||
ir,
|
||||
new RendererError("GRAPH_UNSUPPORTED_FEATURE", {
|
||||
message: "unsupported",
|
||||
path,
|
||||
}),
|
||||
);
|
||||
assert.equal(mapped.path, path);
|
||||
assert.deepEqual(mapped.source, {
|
||||
kind: "parameter",
|
||||
nodeId: "hdr",
|
||||
parameter,
|
||||
test("catalog v8 exposes the final mesh, pipeline, and typed-expression contracts", () => {
|
||||
assert.equal(CATALOG_VERSION, 8);
|
||||
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
||||
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
||||
});
|
||||
assert.equal(semanticCatalog.mesh.version, 2);
|
||||
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
||||
assert.equal(semanticCatalog.pipeline.version, 2);
|
||||
assert.equal(semanticCatalog.pipeline.inputs.predicate.required, false);
|
||||
for (const key of ["and", "xnor", "equals_f32", "greater_than_u32", "combine_vec4",
|
||||
"separate_mat4", "combine_u32_bits", "separate_u32x16", "separate_local_aabb"])
|
||||
assert.equal(semanticCatalog[key].execution, "expression", key);
|
||||
for (const [key, contract] of Object.entries(semanticCatalog)) {
|
||||
assert.equal(nodeDefinitions[key].version, contract.version, key);
|
||||
assert.equal(descriptors[key].version, contract.version, key);
|
||||
}
|
||||
});
|
||||
test("diagnostic mapper creates a frozen RendererError DTO with fallbacks and prefix matching", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const original = new RendererError("GRAPH_INPUT", {
|
||||
message: "bad",
|
||||
field: "nodes[0].executor.key.more",
|
||||
nested: { x: 1 },
|
||||
|
||||
test("current culling fixture uses type-bit predicates and final socket versions", () => {
|
||||
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.cull.inputs.localAabb, { node: "mesh", socket: "localAabb" });
|
||||
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" });
|
||||
assert.equal(byId.ground.executor.version, 2);
|
||||
assert.equal(byId.ground.inputs.predicate.node, "ground_final");
|
||||
});
|
||||
const mapped = mapAuthoringDiagnostic(ir, original);
|
||||
assert.notStrictEqual(mapped, original);
|
||||
assert.equal(mapped.code, original.code);
|
||||
assert.equal(mapped.source.kind, "node");
|
||||
assert.equal(mapped.diagnostic, undefined);
|
||||
assert.ok(Object.isFrozen(mapped) && Object.isFrozen(mapped.details.nested));
|
||||
assert.equal(Object.isFrozen(original), false);
|
||||
original.details.nested.x = 2;
|
||||
assert.equal(mapped.details.nested.x, 1);
|
||||
const unmatchedOriginal = new RendererError("GRAPH_INPUT", {
|
||||
message: "unmapped",
|
||||
path: "resources[0]",
|
||||
});
|
||||
const unmatched = mapAuthoringDiagnostic(ir, unmatchedOriginal);
|
||||
assert.notStrictEqual(unmatched, unmatchedOriginal);
|
||||
assert.equal(unmatched.source, undefined);
|
||||
assert.ok(Object.isFrozen(unmatched) && Object.isFrozen(unmatched.details));
|
||||
assert.equal(Object.isFrozen(unmatchedOriginal), false);
|
||||
});
|
||||
test("controller keeps last-good through failures and only drops after successful switch", async () => {
|
||||
let fail = false;
|
||||
const calls = [];
|
||||
const renderer = {
|
||||
compileGraph: async (ir) => ({
|
||||
compiledId: [ir.revision, 1],
|
||||
revision: ir.revision,
|
||||
}),
|
||||
switchCompiledGraph: async (id) => {
|
||||
calls.push(["switch", id]);
|
||||
if (fail) throw Error("switch");
|
||||
},
|
||||
dropCompiledGraph: async (id) => calls.push(["drop", id]),
|
||||
};
|
||||
const c = new AuthoringController({
|
||||
renderer,
|
||||
adapt: (_, revision) => ({ revision }),
|
||||
});
|
||||
c.markDirty({});
|
||||
await c.apply();
|
||||
fail = true;
|
||||
c.markDirty({});
|
||||
await assert.rejects(c.apply());
|
||||
assert.deepEqual(calls, [
|
||||
["switch", [1, 1]],
|
||||
["switch", [2, 1]],
|
||||
]);
|
||||
fail = false;
|
||||
await c.apply();
|
||||
assert.deepEqual(calls.at(-1), ["drop", [1, 1]]);
|
||||
await c.destroy();
|
||||
});
|
||||
test("controller shares in-flight apply", async () => {
|
||||
let release;
|
||||
const gate = new Promise((r) => (release = r));
|
||||
const c = new AuthoringController({
|
||||
adapt: (_, revision) => ({ revision }),
|
||||
renderer: {
|
||||
compileGraph: async (ir) => {
|
||||
await gate;
|
||||
return { compiledId: [ir.revision, 1], revision: ir.revision };
|
||||
},
|
||||
switchCompiledGraph: async () => {},
|
||||
dropCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.markDirty({});
|
||||
const a = c.apply();
|
||||
assert.strictEqual(c.apply(), a);
|
||||
release();
|
||||
await a;
|
||||
});
|
||||
test("controller retains mapped diagnostic while apply rejects the original and subscriptions agree", async () => {
|
||||
const original = new RendererError("GRAPH_BAD", {
|
||||
path: "nodes[0].id",
|
||||
message: "bad",
|
||||
});
|
||||
const states = [];
|
||||
const c = new AuthoringController({
|
||||
adapt: (snapshot, revision) => adaptFxNodeSnapshot(snapshot, revision),
|
||||
renderer: {
|
||||
compileGraph: async () => {
|
||||
throw original;
|
||||
},
|
||||
switchCompiledGraph: async () => {},
|
||||
dropCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.subscribe((state) => states.push(state));
|
||||
c.markDirty(fixture());
|
||||
await assert.rejects(c.apply(), (error) => error === original);
|
||||
assert.notStrictEqual(states.at(-1).error, original);
|
||||
let subscribed;
|
||||
c.subscribe((state) => {
|
||||
subscribed = state;
|
||||
})();
|
||||
assert.strictEqual(subscribed.error, states.at(-1).error);
|
||||
await c.destroy();
|
||||
});
|
||||
test("apply after destroy does not compile and destroy returns one strict promise", async () => {
|
||||
let compiles = 0;
|
||||
const c = new AuthoringController({
|
||||
adapt: () => ({}),
|
||||
renderer: {
|
||||
compileGraph: async () => {
|
||||
compiles++;
|
||||
},
|
||||
dropCompiledGraph: async () => {},
|
||||
switchCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.markDirty({});
|
||||
const first = c.destroy();
|
||||
assert.strictEqual(c.destroy(), first);
|
||||
assert.strictEqual(await c.apply(), null);
|
||||
await first;
|
||||
assert.equal(compiles, 0);
|
||||
|
||||
test("removed architecture is absent from the authoring catalog", () => {
|
||||
for (const removed of ["mesh_query", "pipeline_registry"])
|
||||
assert.equal(semanticCatalog[removed], undefined);
|
||||
const serialized = JSON.stringify(semanticCatalog);
|
||||
for (const removedSocket of ["isVisible", "localAabbs", "activation"])
|
||||
assert.equal(serialized.includes(`\"${removedSocket}\"`), false);
|
||||
});
|
||||
|
||||
@@ -3,304 +3,39 @@ import assert from "node:assert/strict";
|
||||
import * as presets from "../static/render-graph/presets.js";
|
||||
import { descriptors } from "../static/render-graph/catalog.js";
|
||||
|
||||
const order = [
|
||||
"midnight",
|
||||
"ember",
|
||||
"hdr",
|
||||
"culling",
|
||||
"tone",
|
||||
"contain",
|
||||
"reinhard",
|
||||
"linear",
|
||||
"grading",
|
||||
"edges",
|
||||
"bloom",
|
||||
"combined",
|
||||
];
|
||||
const sequences = {
|
||||
midnight: [
|
||||
["ldr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
ember: [
|
||||
["ldr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
hdr: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
culling: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["cull", "frustum_cull"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
tone: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
grading: [
|
||||
["balance_hdr", "texture"], ["exposure_hdr", "texture"], ["saturation_hdr", "texture"], ["mixer_hdr", "texture"],
|
||||
["hdr", "texture"], ["depth", "texture"], ["mesh", "mesh"], ["query", "mesh_query"], ["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"], ["pbr", "pipeline"], ["pbr_double", "pipeline"], ["balance", "color_balance"], ["exposure", "exposure_contrast"],
|
||||
["saturation", "saturation"], ["mixer", "channel_mixer"], ["frame_out", "frame_out"],
|
||||
],
|
||||
edges: [
|
||||
["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"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
bloom: [
|
||||
["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"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
combined: [
|
||||
["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"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
};
|
||||
for (const name of ["contain", "reinhard", "linear"])
|
||||
sequences[name] = sequences.tone;
|
||||
|
||||
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_contain",
|
||||
"preset_reinhard",
|
||||
"preset_linear",
|
||||
"preset_grading",
|
||||
"preset_edges",
|
||||
"preset_bloom",
|
||||
"preset_combined",
|
||||
],
|
||||
);
|
||||
for (const name of order) {
|
||||
const graph = presets[name];
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [2, 1]);
|
||||
assert.equal(
|
||||
new Set(graph.nodes.map((node) => node.id)).size,
|
||||
graph.nodes.length,
|
||||
);
|
||||
assert.deepEqual(
|
||||
graph.nodes.map((node) => [node.id, node.executor.key]),
|
||||
sequences[name],
|
||||
);
|
||||
assert.equal(
|
||||
graph.nodes.filter((node) => node.executor.key === "frame_out").length,
|
||||
1,
|
||||
);
|
||||
assert.ok(
|
||||
graph.nodes.every(
|
||||
(node) => !["surface_target", "present"].includes(node.executor.key),
|
||||
),
|
||||
);
|
||||
assert.ok(!graph.nodes.some((node) => node.id === "copy"));
|
||||
assert.ok(
|
||||
graph.nodes.every(
|
||||
(node) => node.executor.version === descriptors[node.executor.key].version,
|
||||
),
|
||||
);
|
||||
test("all presets use current schemas, versions, and one frame output", () => {
|
||||
assert.equal(Object.keys(presets.renderGraphPresets).length, 12);
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [2, 1], name);
|
||||
assert.equal(new Set(graph.nodes.map((node) => node.id)).size, graph.nodes.length, name);
|
||||
assert.equal(graph.nodes.filter((node) => node.executor.key === "frame_out").length, 1, name);
|
||||
for (const node of graph.nodes)
|
||||
assert.equal(node.executor.version, descriptors[node.executor.key].version, `${name}:${node.id}`);
|
||||
}
|
||||
const grading = presets.grading;
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "balance").parameters, {
|
||||
mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1],
|
||||
gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1],
|
||||
offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1],
|
||||
slope: 1, slopeColor: [1,1,1,1],
|
||||
});
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "exposure").parameters, { exposureStops: 0, contrast: 1, pivot: .18, factor: 1 });
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "saturation").parameters, { saturation: 1, factor: 1 });
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "mixer").parameters, { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 });
|
||||
});
|
||||
|
||||
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",
|
||||
];
|
||||
test("presets classify visibility and material through type.words[0] predicates", () => {
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.query.parameters, {
|
||||
visiblePredicate: "required_true",
|
||||
visibleDefault: true,
|
||||
frustumCulledPredicate: name === "culling" ? "required_false" : "any",
|
||||
frustumCulledDefault: false,
|
||||
});
|
||||
assert.deepEqual(byId.query.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||
assert.deepEqual(byId.query.inputs.isVisible, {
|
||||
node: "mesh",
|
||||
socket: "isVisible",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.mesh, {
|
||||
node: "mesh",
|
||||
socket: "mesh",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.draws, {
|
||||
node: "query",
|
||||
socket: "draws",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.depthTarget, {
|
||||
node: "depth",
|
||||
socket: "texture",
|
||||
});
|
||||
assert.equal(
|
||||
graph.nodes.filter((node) => node.executor.key === "pipeline_registry")
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
const pipelines = graph.nodes.filter(
|
||||
(node) => node.executor.key === "pipeline",
|
||||
);
|
||||
assert.deepEqual(
|
||||
pipelines.map((node) => node.parameters.pipeline),
|
||||
["ground_plane", "gltf_standard", "gltf_standard_double_sided"],
|
||||
);
|
||||
for (const pipeline of pipelines)
|
||||
assert.deepEqual(pipeline.inputs.activation, {
|
||||
node: "registry",
|
||||
socket: "activation",
|
||||
});
|
||||
assert.deepEqual(byId.pbr.inputs.colorTarget, {
|
||||
node: "ground",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.pbr.inputs.depthTarget, {
|
||||
node: "ground",
|
||||
socket: "depth",
|
||||
});
|
||||
assert.deepEqual(byId.pbr_double.inputs.colorTarget, {
|
||||
node: "pbr",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.pbr_double.inputs.depthTarget, {
|
||||
node: "pbr",
|
||||
socket: "depth",
|
||||
});
|
||||
assert.ok(
|
||||
graph.nodes
|
||||
.filter((node) => node.executor.key === "texture")
|
||||
.every((node) => node.parameters.texture.dimension === "d2"),
|
||||
);
|
||||
assert.ok(
|
||||
graph.nodes.every((node) => !removed.includes(node.executor.key)),
|
||||
);
|
||||
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" }, name);
|
||||
assert.deepEqual(byId.type_bits.inputs.value, { node: "type_words", socket: "word0" }, name);
|
||||
const suffix = name === "culling" ? "_final" : "_class";
|
||||
assert.deepEqual(byId.ground.inputs.predicate, { node: `ground${suffix}`, socket: "value" }, name);
|
||||
assert.deepEqual(byId.pbr.inputs.predicate, { node: name === "culling" ? "pbr_final" : "standard_class", socket: "value" }, name);
|
||||
assert.deepEqual(byId.pbr_double.inputs.predicate, { node: name === "culling" ? "pbr_double_final" : "double_class", socket: "value" }, name);
|
||||
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
||||
assert.deepEqual(pipeline.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||
assert.equal(pipeline.executor.version, 2);
|
||||
}
|
||||
}
|
||||
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", "contain", "reinhard", "linear", "grading", "edges", "bloom", "combined"])
|
||||
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
||||
const finalSource = {
|
||||
hdr: "pbr_double",
|
||||
culling: "pbr_double",
|
||||
tone: "pbr_double",
|
||||
contain: "pbr_double",
|
||||
reinhard: "pbr_double",
|
||||
linear: "pbr_double",
|
||||
grading: "mixer",
|
||||
edges: "edges",
|
||||
bloom: "composite",
|
||||
combined: "edges",
|
||||
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",
|
||||
|
||||
test("culling adds a local-AABB expression to each material predicate", () => {
|
||||
const byId = Object.fromEntries(presets.culling.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.cull.inputs, {
|
||||
mesh: { node: "mesh", socket: "mesh" }, localAabb: { node: "mesh", socket: "localAabb" },
|
||||
});
|
||||
assert.deepEqual(byId.not_culled.inputs.operand, { node: "cull", socket: "isFrustumCulled" });
|
||||
for (const id of ["ground", "pbr", "pbr_double"])
|
||||
assert.equal(byId[id].inputs.predicate.node.endsWith("_final"), true);
|
||||
});
|
||||
|
||||
@@ -10,43 +10,41 @@ class WorkerMock extends EventTarget {
|
||||
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
|
||||
}
|
||||
function fixture() {
|
||||
const memory = new WebAssembly.Memory({initial:2, maximum:4, shared:true});
|
||||
const memory = new WebAssembly.Memory({initial:4, maximum:8, shared:true});
|
||||
const header = new Int32Array(memory.buffer, 0, 16);
|
||||
header.set([0x4e574159,1,1024,24,0,0]);
|
||||
header.set([0x4e574159,2,1024,40,0,0]);
|
||||
const worker = new WorkerMock();
|
||||
const bridge = {memory,ringPtr:0,worker,freed:false,free(){this.freed=true;}};
|
||||
const client = new RendererClient(bridge);
|
||||
return {memory,header,worker,bridge,client};
|
||||
}
|
||||
async function imported(f) {
|
||||
const loading=f.client.importGlb(new ArrayBuffer(8));
|
||||
const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
|
||||
f.worker.reply({type:"payload-ready",id:1});
|
||||
await Promise.resolve();
|
||||
f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[[7,3]]}});
|
||||
f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[{handle:[7,3],defaultType:[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}});
|
||||
return (await loading)[0];
|
||||
}
|
||||
test("replaceSceneGlb is opcode 1 and importGlb remains an alias", async()=>{
|
||||
for(const method of ["replaceSceneGlb","importGlb"]){const f=fixture(),pending=f.client[method](new ArrayBuffer(8));f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.equal(new Int32Array(f.memory.buffer,64,24)[1],1);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});assert.deepEqual(await pending,[]);}
|
||||
});
|
||||
test("scene replacement carries the framing mode in opcode 1",async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"interior"});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.deepEqual([...new Int32Array(f.memory.buffer,64,24).slice(1,5)],[1,1,1,1]);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});await pending;await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"bad"}),TypeError)});
|
||||
test("replaceSceneGlb is opcode 1", async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8));f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.equal(new Int32Array(f.memory.buffer,64,40)[1],1);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});assert.deepEqual(await pending,[]);});
|
||||
test("scene replacement carries the framing mode in opcode 1",async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"interior"});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.deepEqual([...new Int32Array(f.memory.buffer,64,40).slice(1,5)],[1,1,1,1]);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});await pending;await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"bad"}),TypeError)});
|
||||
test("writes tagged fixed-slot protocol and resolves reply", async () => {
|
||||
const f=fixture(); const mesh=await imported(f);
|
||||
const pending=mesh.setVisible(true);
|
||||
const {memory,header,worker}=f;
|
||||
assert.equal(Atomics.load(header,5),2);
|
||||
const slot=new Int32Array(memory.buffer,64+96,24);
|
||||
assert.deepEqual([...slot.slice(0,6)],[1,2,2,7,3,1]);
|
||||
const slot=new Int32Array(memory.buffer,64+160,40);
|
||||
assert.deepEqual([...slot.slice(0,6)],[2,2,2,7,3,1]);
|
||||
worker.reply({type:"reply",request:2,ok:true,code:"OK"}); await pending;
|
||||
});
|
||||
test("maps stable errors and gates destroyed instances", async () => {
|
||||
const f=fixture(), mesh=await imported(f); const {worker}=f;
|
||||
const creating=mesh.createInstance(new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),false);
|
||||
const creating=mesh.createInstance(new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),{visible:false});
|
||||
worker.reply({type:"reply",request:2,ok:true,result:[4,2]}); const instance=await creating;
|
||||
const destroying=instance.destroy(); worker.reply({type:"reply",request:3,ok:true}); await destroying;
|
||||
assert.throws(()=>instance.setVisible(true), error=>error instanceof RendererError&&error.code==="STALE_HANDLE");
|
||||
});
|
||||
test("rejects protocol mismatch", () => {
|
||||
const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=2;
|
||||
const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=1;
|
||||
assert.throws(()=>new RendererClient({memory,ringPtr:0,worker}), /PROTOCOL_MISMATCH/);
|
||||
});
|
||||
test("pending reply exists before ring publication", async () => {
|
||||
@@ -70,7 +68,7 @@ test("worker failures and dispose reject every pending operation", async () => {
|
||||
});
|
||||
test("import always releases staged payload when ring is full", async () => {
|
||||
const {header,worker,client}=fixture(); Atomics.store(header,5,1024);
|
||||
const loading=client.importGlb(new ArrayBuffer(8));
|
||||
const loading=client.replaceSceneGlb(new ArrayBuffer(8));
|
||||
worker.reply({type:"payload-ready",id:1});
|
||||
await assert.rejects(loading,/RING_FULL/);
|
||||
assert.equal(worker.messages.at(-1).type,"payload-release");
|
||||
@@ -84,7 +82,7 @@ test("does not export handle constructors or internal mutation methods", () => {
|
||||
});
|
||||
test("corrupt backlog closes and terminates the transport", async () => {
|
||||
const f=fixture(); Atomics.store(f.header,5,1025);
|
||||
const loading=f.client.importGlb(new ArrayBuffer(8));
|
||||
const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
|
||||
// Payload staging must first acknowledge before enqueue sees corruption.
|
||||
f.worker.reply({type:"payload-ready",id:1});
|
||||
await assert.rejects(loading,/RING_CORRUPT/);
|
||||
@@ -94,7 +92,7 @@ test("corrupt backlog closes and terminates the transport", async () => {
|
||||
test("import rejects immediately after disposal", async () => {
|
||||
const f=fixture();
|
||||
f.client.dispose();
|
||||
await assert.rejects(f.client.importGlb(new ArrayBuffer(8)),/DISPOSED/);
|
||||
await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8)),/DISPOSED/);
|
||||
assert.equal(f.worker.messages.length,0);
|
||||
});
|
||||
test("import rejects when disposed during asynchronous source loading", async () => {
|
||||
@@ -103,7 +101,7 @@ test("import rejects when disposed during asynchronous source loading", async ()
|
||||
let finishFetch;
|
||||
globalThis.fetch=()=>new Promise(resolve=>{finishFetch=resolve;});
|
||||
try {
|
||||
const loading=f.client.importGlb("model.glb");
|
||||
const loading=f.client.replaceSceneGlb("model.glb");
|
||||
f.client.dispose();
|
||||
finishFetch({arrayBuffer:async()=>new ArrayBuffer(8)});
|
||||
await assert.rejects(loading,/DISPOSED/);
|
||||
@@ -117,7 +115,7 @@ test("compile transfers payload and waits for ready before opcode 7", async()=>{
|
||||
const f=fixture(), pending=f.client.compileGraph({schemaVersion:2});
|
||||
assert.equal(f.worker.transfers[0].length,1); assert.equal(Atomics.load(f.header,5),0);
|
||||
f.worker.reply({type:"payload-ready",id:1}); await Promise.resolve();
|
||||
assert.equal(new Int32Array(f.memory.buffer,64,24)[1],7);
|
||||
assert.equal(new Int32Array(f.memory.buffer,64,40)[1],7);
|
||||
f.worker.reply({type:"reply",request:1,ok:true,result:{compiledId:[2,3]}});
|
||||
assert.deepEqual(await pending,{compiledId:[2,3]});
|
||||
});
|
||||
@@ -130,12 +128,12 @@ test("compile releases payload after success", async()=>{const f=fixture(),p=f.c
|
||||
test("compile releases payload after backend error", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();f.worker.reply({type:"reply",request:1,ok:false,code:"X",details:{message:"x"}});await assert.rejects(p);assert.equal(f.worker.messages.at(-1).type,"payload-release");});
|
||||
test("compile rejects circular and BigInt JSON", async()=>{const f=fixture(),x={};x.x=x;await assert.rejects(f.client.compileGraph(x),/circular/i);await assert.rejects(f.client.compileGraph({x:1n}),e=>e.code==="GRAPH_JSON_INVALID");});
|
||||
test("compile rejects oversized encoding", async()=>{const f=fixture();await assert.rejects(f.client.compileGraph({x:"x".repeat(1024*1024)}),e=>e.code==="GRAPH_PAYLOAD_TOO_LARGE");assert.equal(f.worker.messages.length,0);});
|
||||
test("drop graph emits opcode 8 and validates id", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([9,4]);const slot=new Int32Array(f.memory.buffer,64,24);assert.deepEqual([...slot.slice(1,5)],[8,1,9,4]);f.worker.reply({type:"reply",request:1,ok:true});await p;assert.throws(()=>f.client.dropCompiledGraph([1]),TypeError);});
|
||||
test("drop graph emits opcode 8 and validates id", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([9,4]);const slot=new Int32Array(f.memory.buffer,64,40);assert.deepEqual([...slot.slice(1,5)],[8,1,9,4]);f.worker.reply({type:"reply",request:1,ok:true});await p;assert.throws(()=>f.client.dropCompiledGraph([1]),TypeError);});
|
||||
test("ring-full graph compile releases staged payload", async()=>{const f=fixture();Atomics.store(f.header,5,1024);const p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await assert.rejects(p,e=>e.code==="RING_FULL");assert.equal(f.worker.messages.at(-1).type,"payload-release");});
|
||||
test("disposal while graph payload is pending releases and rejects", async()=>{const f=fixture(),p=f.client.compileGraph({});f.client.dispose();await assert.rejects(p,e=>e.code==="DISPOSED");assert.equal(f.worker.messages.at(-1).type,"payload-release");});
|
||||
test("payload transfer uses the exact encoded ArrayBuffer", async()=>{const f=fixture(),graph={schemaVersion:2};const expected=new TextEncoder().encode(JSON.stringify(graph));const p=f.client.compileGraph(graph);const transferred=f.worker.transfers[0][0];assert.strictEqual(f.worker.messages[0].buffer,transferred);assert.deepEqual(new Uint8Array(transferred),expected);f.client.dispose();await assert.rejects(p);});
|
||||
test("cycle error details are preserved exactly", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();const details={message:"cycle",kind:"cycle",edges:[{from:"a",resource:{id:"r",version:0},to:"b"}]};f.worker.reply({type:"reply",request:1,ok:false,code:"GRAPH_CYCLE",details});await assert.rejects(p,e=>e.details===details&&e.details.edges[0].from==="a");});
|
||||
test("error without details leaves details undefined", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([1,1]);f.worker.reply({type:"reply",request:1,ok:false,code:"STALE_GRAPH_ID"});await assert.rejects(p,e=>e instanceof RendererError&&e.details===undefined&&e.message==="STALE_GRAPH_ID");});
|
||||
test("switch methods emit exact opcode 9 words", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([9,4]);assert.deepEqual([...new Int32Array(f.memory.buffer,64,24).slice(1,7)],[9,1,1,9,4,0]);f.worker.reply({type:"reply",request:1,ok:true});await a;await new Promise(queueMicrotask);const b=f.client.switchToImmediate();assert.deepEqual([...new Int32Array(f.memory.buffer,160,24).slice(1,7)],[9,2,0,0,0,0]);f.worker.reply({type:"reply",request:2,ok:true});await b;assert.throws(()=>f.client.switchCompiledGraph([0,0]),TypeError);});
|
||||
test("switch methods emit exact opcode 9 words", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([9,4]);assert.deepEqual([...new Int32Array(f.memory.buffer,64,40).slice(1,7)],[9,1,1,9,4,0]);f.worker.reply({type:"reply",request:1,ok:true});await a;await new Promise(queueMicrotask);const b=f.client.switchToImmediate();assert.deepEqual([...new Int32Array(f.memory.buffer,224,40).slice(1,7)],[9,2,0,0,0,0]);f.worker.reply({type:"reply",request:2,ok:true});await b;assert.throws(()=>f.client.switchCompiledGraph([0,0]),TypeError);});
|
||||
test("graph lifecycle FIFO recovers after failure", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([1,1]),b=f.client.switchToImmediate();assert.equal(Atomics.load(f.header,5),1);f.worker.reply({type:"reply",request:1,ok:false,code:"X"});await assert.rejects(a);await new Promise(queueMicrotask);assert.equal(Atomics.load(f.header,5),2);f.worker.reply({type:"reply",request:2,ok:true});await b;});
|
||||
test("dispose rejects queued graph lifecycle calls", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([1,1]),b=f.client.switchToImmediate();f.client.dispose();await assert.rejects(a,/DISPOSED/);await assert.rejects(b,/DISPOSED/);});
|
||||
|
||||
+17
-17
@@ -5,37 +5,37 @@ import { DerivedBvh } from "../static/bvh-core.js";
|
||||
import { RendererClient } from "../static/renderer-client.js";
|
||||
|
||||
const align16 = value => (value + 15) & ~15;
|
||||
const componentCounts = [1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
|
||||
const scalarTypes = [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
const componentCounts = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
|
||||
const scalarTypes = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
|
||||
function snapshotFixture({ instances = 1 } = {}) {
|
||||
const memory = new WebAssembly.Memory({ initial: 4, maximum: 8, shared: true });
|
||||
const words = new Uint32Array(memory.buffer);
|
||||
const control = new Int32Array(memory.buffer, 0, 64);
|
||||
const ptr = 256;
|
||||
const counts = [1, 1, 1, 1, 1, ...Array(9).fill(instances)];
|
||||
const counts = [1, 1, 1, 1, ...Array(8).fill(instances)];
|
||||
const offsets = [];
|
||||
let cursor = 512;
|
||||
for (let i = 0; i < 14; i++) {
|
||||
let cursor = 448;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
offsets.push(cursor);
|
||||
cursor = align16(cursor + counts[i] * componentCounts[i] * 4);
|
||||
}
|
||||
control.set([0x504e5359, 1, 256, 3, 64, 1, 1, 2, 1, 0, 7, 0, 4, 1, 0, 0]);
|
||||
control.set([2, 1, 1, ptr, cursor, 7, 0, 1, instances, 1, 64, 0, 0, 0, 0, 0], 16);
|
||||
control.set([0x504e5359, 1, 256, 3, 64, 2, 1, 2, 1, 0, 7, 0, 4, 1, 0, 0]);
|
||||
control.set([2, 1, 1, ptr, cursor, 7, 0, 1, instances, 2, 64, 0, 0, 0, 0, 0], 16);
|
||||
const blob = new Uint32Array(memory.buffer, ptr, cursor / 4);
|
||||
blob.set([0x31534452, 1, 64, cursor, 1, 7, 0, 14, 64, 32, 1, instances, 0x01020304, 3, 0, 0]);
|
||||
for (let i = 0; i < 14; i++) {
|
||||
blob.set([0x32534452, 2, 64, cursor, 1, 7, 0, 12, 64, 32, 1, instances, 0x01020304, 3, 0, 0]);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
blob.set([i + 1, scalarTypes[i], offsets[i], counts[i], componentCounts[i], componentCounts[i] * 4, 4, 0], 16 + i * 8);
|
||||
}
|
||||
const stream = i => scalarTypes[i] === 2
|
||||
? new Float32Array(memory.buffer, ptr + offsets[i], counts[i] * componentCounts[i])
|
||||
: new Uint32Array(memory.buffer, ptr + offsets[i], counts[i] * componentCounts[i]);
|
||||
stream(0)[0] = 4; stream(1)[0] = 2; stream(2)[0] = 1;
|
||||
stream(3).set([-1, -1, -1]); stream(4).set([1, 1, 1]);
|
||||
stream(0)[0] = 4; stream(1)[0] = 2;
|
||||
stream(2).set([-1, -1, -1]); stream(3).set([1, 1, 1]);
|
||||
for (let i = 0; i < instances; i++) {
|
||||
stream(5)[i] = 10 + i; stream(6)[i] = 3; stream(7)[i] = 4; stream(8)[i] = 2;
|
||||
stream(9)[i] = 1; stream(13)[i] = 1;
|
||||
stream(11).set([i * 4, -1, -1], i * 3); stream(12).set([i * 4 + 2, 1, 1], i * 3);
|
||||
stream(4)[i] = 10 + i; stream(5)[i] = 3; stream(6)[i] = 4; stream(7)[i] = 2;
|
||||
stream(11)[i * 16] = 1;
|
||||
stream(9).set([i * 4, -1, -1], i * 3); stream(10).set([i * 4 + 2, 1, 1], i * 3);
|
||||
}
|
||||
return { memory, control, ptr, cursor };
|
||||
}
|
||||
@@ -84,7 +84,7 @@ function bvhSnapshot({ pickable = [1, 1], shifted = false } = {}) {
|
||||
return { instanceCount: count, streams: {
|
||||
instanceSlot: Uint32Array.from([5, 6]), instanceGeneration: Uint32Array.from([1, 1]),
|
||||
instanceMeshSlot: Uint32Array.from([2, 2]), instanceMeshGeneration: Uint32Array.from([4, 4]),
|
||||
instancePickable: Uint32Array.from(pickable),
|
||||
instanceType: Uint32Array.from(pickable.flatMap(x => [x, ...Array(15).fill(0)])),
|
||||
instanceWorldMin: Float32Array.from(shifted ? [10, -1, -1, 4, -1, -1] : [2, -1, -1, 4, -1, -1]),
|
||||
instanceWorldMax: Float32Array.from(shifted ? [12, 1, 1, 6, 1, 1] : [3, 1, 1, 6, 1, 1]),
|
||||
}};
|
||||
@@ -112,11 +112,11 @@ test("renderer pick returns gated instances and exact epoch", async () => {
|
||||
const scene = snapshotFixture();
|
||||
const ring = 8192;
|
||||
const ringHeader = new Int32Array(scene.memory.buffer, ring, 16);
|
||||
ringHeader.set([0x4e574159, 1, 1024, 24]);
|
||||
ringHeader.set([0x4e574159, 2, 1024, 40]);
|
||||
const rendererWorker = new WorkerMock(), bvhWorker = new WorkerMock();
|
||||
const bridge = { memory: scene.memory, ringPtr: ring, worker: rendererWorker, workerFactory: () => bvhWorker, free() {} };
|
||||
const client = new RendererClient(bridge);
|
||||
rendererWorker.reply({ type: "snapshot-init", controlPtr: 0, controlVersion: 1, schemaVersion: 1 });
|
||||
rendererWorker.reply({ type: "snapshot-init", controlPtr: 0, controlVersion: 1, schemaVersion: 2 });
|
||||
rendererWorker.reply({ type: "snapshot-published", epoch: 1 });
|
||||
const picking = client.pickRay([0, 0, 0], [1, 0, 0]);
|
||||
const request = bvhWorker.messages.find(message => message.type === "pick");
|
||||
|
||||
Reference in New Issue
Block a user