refactor: make instance type metadata opaque
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:
@@ -59,30 +59,13 @@ impl MaterialKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct InstanceType {
|
pub struct InstanceType {
|
||||||
pub words: [u32; 16],
|
pub words: [u32; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceType {
|
impl InstanceType {
|
||||||
pub const VISIBLE_MASK: u32 = 1;
|
|
||||||
pub const ZERO: Self = Self { words: [0; 16] };
|
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 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 Default for InstanceType {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::VISIBLE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const _: [(); 64] = [(); std::mem::size_of::<InstanceType>()];
|
const _: [(); 64] = [(); std::mem::size_of::<InstanceType>()];
|
||||||
@@ -685,32 +668,6 @@ impl RenderData {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_mesh_visible(
|
|
||||||
&mut self,
|
|
||||||
handle: MeshHandle,
|
|
||||||
visible: bool,
|
|
||||||
) -> Result<(), RenderDataError> {
|
|
||||||
if !self
|
|
||||||
.meshes
|
|
||||||
.slots
|
|
||||||
.contains(handle.slot(), handle.generation())
|
|
||||||
{
|
|
||||||
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()?;
|
|
||||||
for slot in owned {
|
|
||||||
self.instances.instance_types[slot as usize].set_visible(visible);
|
|
||||||
}
|
|
||||||
self.revision = next_revision;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_instance_type(
|
pub fn set_instance_type(
|
||||||
&mut self,
|
&mut self,
|
||||||
handle: InstanceHandle,
|
handle: InstanceHandle,
|
||||||
@@ -729,24 +686,6 @@ impl RenderData {
|
|||||||
Ok(())
|
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_instance_transform(
|
pub fn set_instance_transform(
|
||||||
&mut self,
|
&mut self,
|
||||||
handle: InstanceHandle,
|
handle: InstanceHandle,
|
||||||
|
|||||||
@@ -104,7 +104,28 @@ fn default_instance_is_protected_and_preserves_its_type() {
|
|||||||
data.destroy_instance(created.default_instance),
|
data.destroy_instance(created.default_instance),
|
||||||
Err(RenderDataError::CannotDestroyDefaultInstance)
|
Err(RenderDataError::CannotDestroyDefaultInstance)
|
||||||
);
|
);
|
||||||
data.set_mesh_visible(created.mesh, false).unwrap();
|
let replacement = InstanceType {
|
||||||
|
words: [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
8,
|
||||||
|
0x8000_0000,
|
||||||
|
u32::MAX,
|
||||||
|
17,
|
||||||
|
31,
|
||||||
|
63,
|
||||||
|
127,
|
||||||
|
255,
|
||||||
|
511,
|
||||||
|
1023,
|
||||||
|
2047,
|
||||||
|
4095,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
data.set_instance_type(created.default_instance, replacement)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.mesh(created.mesh).unwrap().default_instance_type,
|
data.mesh(created.mesh).unwrap().default_instance_type,
|
||||||
info().default_instance_type
|
info().default_instance_type
|
||||||
@@ -113,14 +134,16 @@ fn default_instance_is_protected_and_preserves_its_type() {
|
|||||||
data.instance(created.default_instance)
|
data.instance(created.default_instance)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.instance_type,
|
.instance_type,
|
||||||
{
|
replacement
|
||||||
let mut expected = info().default_instance_type;
|
|
||||||
expected.set_visible(false);
|
|
||||||
expected
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_type_default_is_exactly_zero() {
|
||||||
|
assert_eq!(InstanceType::default(), InstanceType::ZERO);
|
||||||
|
assert_eq!(InstanceType::default().words, [0; 16]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_mesh_and_instance_handles_are_rejected_after_reuse() {
|
fn stale_mesh_and_instance_handles_are_rejected_after_reuse() {
|
||||||
let mut data = data();
|
let mut data = data();
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ fn encode_scene<'a, T: Scene>(
|
|||||||
pass.set_vertex_buffer(4, t.slice(..));
|
pass.set_vertex_buffer(4, t.slice(..));
|
||||||
pass.set_index_buffer(i.slice(..), wgpu::IndexFormat::Uint32);
|
pass.set_index_buffer(i.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
for draw in &gpu.draws {
|
for draw in &gpu.draws {
|
||||||
if !draw.instance_type.is_visible() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
pass.set_pipeline(pipelines.get_pipeline(draw.pipeline));
|
pass.set_pipeline(pipelines.get_pipeline(draw.pipeline));
|
||||||
if pipelines.requires_material(draw.pipeline) {
|
if pipelines.requires_material(draw.pipeline) {
|
||||||
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
pass.set_bind_group(2, materials.group(draw.material), &[]);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::mem::size_of;
|
|||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
render_data::{InstanceType, MaterialKey, MeshHandle, PipelineKey},
|
render_data::{MaterialKey, MeshHandle, PipelineKey},
|
||||||
renderer::scene_frame::SceneFramePlan,
|
renderer::scene_frame::SceneFramePlan,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,7 +24,6 @@ pub struct DrawItem {
|
|||||||
pub indices: std::ops::Range<u32>,
|
pub indices: std::ops::Range<u32>,
|
||||||
pub base_vertex: i32,
|
pub base_vertex: i32,
|
||||||
pub instances: std::ops::Range<u32>,
|
pub instances: std::ops::Range<u32>,
|
||||||
pub instance_type: InstanceType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -169,7 +168,6 @@ impl GpuScenePlan {
|
|||||||
.ok_or("draw range overflow")?,
|
.ok_or("draw range overflow")?,
|
||||||
base_vertex,
|
base_vertex,
|
||||||
instances: instance_index..instance_index + 1,
|
instances: instance_index..instance_index + 1,
|
||||||
instance_type: occurrence.instance_type,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1511,22 +1511,31 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
let meshes = js_sys::Array::new();
|
let meshes = js_sys::Array::new();
|
||||||
for h in installed.meshes {
|
for h in installed.meshes {
|
||||||
let item = js_sys::Object::new();
|
let item = js_sys::Object::new();
|
||||||
|
let view = self.render_data.mesh(h).unwrap();
|
||||||
js_sys::Reflect::set(
|
js_sys::Reflect::set(
|
||||||
&item,
|
&item,
|
||||||
&"handle".into(),
|
&"handle".into(),
|
||||||
&js_sys::Array::of2(&h.slot().into(), &h.generation().into()),
|
&js_sys::Array::of2(&h.slot().into(), &h.generation().into()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let ty = self
|
js_sys::Reflect::set(
|
||||||
.render_data
|
&item,
|
||||||
.mesh(h)
|
&"defaultInstance".into(),
|
||||||
.unwrap()
|
&js_sys::Array::of2(
|
||||||
.default_instance_type
|
&view.default_instance.slot().into(),
|
||||||
.words;
|
&view.default_instance.generation().into(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
js_sys::Reflect::set(
|
js_sys::Reflect::set(
|
||||||
&item,
|
&item,
|
||||||
&"defaultType".into(),
|
&"defaultType".into(),
|
||||||
&js_sys::Array::from_iter(ty.into_iter().map(JsValue::from)),
|
&js_sys::Array::from_iter(
|
||||||
|
view.default_instance_type
|
||||||
|
.words
|
||||||
|
.into_iter()
|
||||||
|
.map(JsValue::from),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
meshes.push(&item);
|
meshes.push(&item);
|
||||||
@@ -1534,19 +1543,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
js_sys::Reflect::set(&result, &"meshes".into(), &meshes).unwrap();
|
js_sys::Reflect::set(&result, &"meshes".into(), &meshes).unwrap();
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
2 => {
|
|
||||||
self.render_data
|
|
||||||
.set_mesh_visible(
|
|
||||||
MeshHandle::from_parts(words[2], words[3]),
|
|
||||||
match words[4] {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => return Err("INVALID_VISIBILITY"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(|e| render_data_error_code(&e))?;
|
|
||||||
Ok(JsValue::UNDEFINED)
|
|
||||||
}
|
|
||||||
3 => {
|
3 => {
|
||||||
let mesh = MeshHandle::from_parts(words[2], words[3]);
|
let mesh = MeshHandle::from_parts(words[2], words[3]);
|
||||||
let mut m = [[0.; 4]; 4];
|
let mut m = [[0.; 4]; 4];
|
||||||
@@ -1565,19 +1561,6 @@ impl<T: Scene + 'static> Renderer<T> {
|
|||||||
.map_err(|e| render_data_error_code(&e))?;
|
.map_err(|e| render_data_error_code(&e))?;
|
||||||
Ok(js_sys::Array::of2(&h.slot().into(), &h.generation().into()).into())
|
Ok(js_sys::Array::of2(&h.slot().into(), &h.generation().into()).into())
|
||||||
}
|
}
|
||||||
4 => {
|
|
||||||
self.render_data
|
|
||||||
.set_instance_visible(
|
|
||||||
InstanceHandle::from_parts(words[2], words[3]),
|
|
||||||
match words[4] {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => return Err("INVALID_VISIBILITY"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(|e| render_data_error_code(&e))?;
|
|
||||||
Ok(JsValue::UNDEFINED)
|
|
||||||
}
|
|
||||||
5 => {
|
5 => {
|
||||||
let h = InstanceHandle::from_parts(words[2], words[3]);
|
let h = InstanceHandle::from_parts(words[2], words[3]);
|
||||||
let mut m = [[0.; 4]; 4];
|
let mut m = [[0.; 4]; 4];
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::render_data::{MeshCreateInfo, RenderDataConfig, IDENTITY_MODEL_TRANSFORM};
|
use crate::render_data::{MeshCreateInfo, RenderDataConfig, IDENTITY_MODEL_TRANSFORM};
|
||||||
|
|
||||||
fn mesh(data: &mut RenderData, visible: bool) -> crate::render_data::CreatedMesh {
|
fn mesh(data: &mut RenderData, instance_type: InstanceType) -> crate::render_data::CreatedMesh {
|
||||||
data.create_mesh(MeshCreateInfo {
|
data.create_mesh(MeshCreateInfo {
|
||||||
positions: &[[0., 0., 0.], [2., 0., 0.], [0., 2., 0.]],
|
positions: &[[0., 0., 0.], [2., 0., 0.], [0., 2., 0.]],
|
||||||
normals: &[[0., 0., 1.]; 3],
|
normals: &[[0., 0., 1.]; 3],
|
||||||
@@ -170,11 +170,7 @@ mod tests {
|
|||||||
indices: &[0, 1, 2],
|
indices: &[0, 1, 2],
|
||||||
pipeline: PipelineKey::new(0),
|
pipeline: PipelineKey::new(0),
|
||||||
material: crate::render_data::MaterialKey::DEFAULT,
|
material: crate::render_data::MaterialKey::DEFAULT,
|
||||||
default_instance_type: if visible {
|
default_instance_type: instance_type,
|
||||||
InstanceType::VISIBLE
|
|
||||||
} else {
|
|
||||||
InstanceType::ZERO
|
|
||||||
},
|
|
||||||
default_transform: IDENTITY_MODEL_TRANSFORM,
|
default_transform: IDENTITY_MODEL_TRANSFORM,
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -186,10 +182,11 @@ mod tests {
|
|||||||
let mut cache = SceneFrameCache::default();
|
let mut cache = SceneFrameCache::default();
|
||||||
let first = cache.get_or_build(&data).unwrap() as *const _;
|
let first = cache.get_or_build(&data).unwrap() as *const _;
|
||||||
assert_eq!(first, cache.get_or_build(&data).unwrap() as *const _);
|
assert_eq!(first, cache.get_or_build(&data).unwrap() as *const _);
|
||||||
let created = mesh(&mut data, true);
|
let created = mesh(&mut data, InstanceType::ZERO);
|
||||||
let second = cache.get_or_build(&data).unwrap() as *const _;
|
let second = cache.get_or_build(&data).unwrap() as *const _;
|
||||||
assert_ne!(first, second);
|
assert_ne!(first, second);
|
||||||
data.set_mesh_visible(created.mesh, false).unwrap();
|
data.set_instance_type(created.default_instance, InstanceType { words: [5; 16] })
|
||||||
|
.unwrap();
|
||||||
let third = cache.get_or_build(&data).unwrap() as *const _;
|
let third = cache.get_or_build(&data).unwrap() as *const _;
|
||||||
assert_ne!(second, third);
|
assert_ne!(second, third);
|
||||||
let mut moved = IDENTITY_MODEL_TRANSFORM;
|
let mut moved = IDENTITY_MODEL_TRANSFORM;
|
||||||
@@ -200,10 +197,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retains_hidden_entries_builds_adjacency_and_world_bounds() {
|
fn retains_all_entries_and_preserves_opaque_types() {
|
||||||
let mut data = RenderData::new(RenderDataConfig::default()).unwrap();
|
let mut data = RenderData::new(RenderDataConfig::default()).unwrap();
|
||||||
let hidden = mesh(&mut data, false);
|
let zero = mesh(&mut data, InstanceType::ZERO);
|
||||||
let shown = mesh(&mut data, true);
|
let marked = mesh(
|
||||||
|
&mut data,
|
||||||
|
InstanceType {
|
||||||
|
words: [0, u32::MAX, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9],
|
||||||
|
},
|
||||||
|
);
|
||||||
let mut translated = IDENTITY_MODEL_TRANSFORM;
|
let mut translated = IDENTITY_MODEL_TRANSFORM;
|
||||||
translated[0][0] = 2.;
|
translated[0][0] = 2.;
|
||||||
translated[1][1] = 3.;
|
translated[1][1] = 3.;
|
||||||
@@ -211,14 +213,14 @@ mod tests {
|
|||||||
translated[3][0] = 5.;
|
translated[3][0] = 5.;
|
||||||
translated[3][1] = -2.;
|
translated[3][1] = -2.;
|
||||||
let extra = data
|
let extra = data
|
||||||
.create_instance(hidden.mesh, translated, InstanceType::ZERO)
|
.create_instance(zero.mesh, translated, InstanceType::ZERO)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let plan = SceneFramePlan::build(&data).unwrap();
|
let plan = SceneFramePlan::build(&data).unwrap();
|
||||||
assert_eq!((plan.meshes.len(), plan.occurrences.len()), (2, 3));
|
assert_eq!((plan.meshes.len(), plan.occurrences.len()), (2, 3));
|
||||||
assert!(plan
|
assert!(plan
|
||||||
.occurrences
|
.occurrences
|
||||||
.iter()
|
.iter()
|
||||||
.any(|o| o.handle == hidden.default_instance && o.is_default));
|
.any(|o| o.handle == zero.default_instance && o.is_default));
|
||||||
let occurrence = plan.occurrences.iter().find(|o| o.handle == extra).unwrap();
|
let occurrence = plan.occurrences.iter().find(|o| o.handle == extra).unwrap();
|
||||||
assert_eq!(occurrence.world_aabb.min, [5., -2., 0.]);
|
assert_eq!(occurrence.world_aabb.min, [5., -2., 0.]);
|
||||||
assert_eq!(occurrence.world_aabb.max, [9., 4., 0.]);
|
assert_eq!(occurrence.world_aabb.max, [9., 4., 0.]);
|
||||||
@@ -228,10 +230,10 @@ mod tests {
|
|||||||
.all(|&i| plan.occurrences[i].mesh_index == mesh_index));
|
.all(|&i| plan.occurrences[i].mesh_index == mesh_index));
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
shown.default_instance.slot(),
|
marked.default_instance.slot(),
|
||||||
plan.occurrences
|
plan.occurrences
|
||||||
.iter()
|
.iter()
|
||||||
.find(|o| o.mesh == shown.mesh)
|
.find(|o| o.mesh == marked.mesh)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.handle
|
.handle
|
||||||
.slot()
|
.slot()
|
||||||
@@ -241,26 +243,26 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn slot_reuse_preserves_dense_order_adjacency_and_ownership() {
|
fn slot_reuse_preserves_dense_order_adjacency_and_ownership() {
|
||||||
let mut data = RenderData::new(RenderDataConfig::default()).unwrap();
|
let mut data = RenderData::new(RenderDataConfig::default()).unwrap();
|
||||||
let a = mesh(&mut data, true);
|
let a = mesh(&mut data, InstanceType::ZERO);
|
||||||
let doomed = mesh(&mut data, true);
|
let doomed = mesh(&mut data, InstanceType::ZERO);
|
||||||
let c = mesh(&mut data, true);
|
let c = mesh(&mut data, InstanceType::ZERO);
|
||||||
let a_extra = data
|
let a_extra = data
|
||||||
.create_instance(a.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::VISIBLE)
|
.create_instance(a.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let doomed_extra = data
|
let doomed_extra = data
|
||||||
.create_instance(doomed.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::VISIBLE)
|
.create_instance(doomed.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let c_extra = data
|
let c_extra = data
|
||||||
.create_instance(c.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::VISIBLE)
|
.create_instance(c.mesh, IDENTITY_MODEL_TRANSFORM, InstanceType::ZERO)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
data.destroy_instance(a_extra).unwrap();
|
data.destroy_instance(a_extra).unwrap();
|
||||||
data.destroy_mesh(doomed.mesh).unwrap();
|
data.destroy_mesh(doomed.mesh).unwrap();
|
||||||
let replacement = mesh(&mut data, true);
|
let replacement = mesh(&mut data, InstanceType::ZERO);
|
||||||
let replacement_extra = data
|
let replacement_extra = data
|
||||||
.create_instance(
|
.create_instance(
|
||||||
replacement.mesh,
|
replacement.mesh,
|
||||||
IDENTITY_MODEL_TRANSFORM,
|
IDENTITY_MODEL_TRANSFORM,
|
||||||
InstanceType::VISIBLE,
|
InstanceType::ZERO,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(replacement.mesh.slot(), doomed.mesh.slot());
|
assert_eq!(replacement.mesh.slot(), doomed.mesh.slot());
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
export class DerivedBvh {
|
export class DerivedBvh {
|
||||||
constructor() { this.count = 0; this.identity = new Uint32Array(); this.meshIdentity = new Uint32Array(); this.pickable = new Uint8Array(); this.bounds = new Float32Array(); this.nodeBounds = new Float32Array(); this.left = this.right = new Int32Array(); this.leafStart = this.leafCount = new Uint32Array(); this.leaves = new Uint32Array(); this.root = -1; this.rebuilds = 0; this.refits = 0; }
|
constructor() { this.count = 0; this.identity = new Uint32Array(); this.meshIdentity = new Uint32Array(); this.bounds = new Float32Array(); this.nodeBounds = new Float32Array(); this.left = this.right = new Int32Array(); this.leafStart = this.leafCount = new Uint32Array(); this.leaves = new Uint32Array(); this.root = -1; this.rebuilds = 0; this.refits = 0; }
|
||||||
update(snapshot) {
|
update(snapshot) {
|
||||||
const s = snapshot.streams, n = snapshot.instanceCount;
|
const s = snapshot.streams, n = snapshot.instanceCount;
|
||||||
let changed = n !== this.count;
|
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; }
|
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);
|
this.count = n; this.identity = new Uint32Array(n * 2); this.meshIdentity = new Uint32Array(n * 2); 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.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); }
|
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.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();
|
changed ? this.rebuild() : this.refit();
|
||||||
}
|
}
|
||||||
rebuild() {
|
rebuild() {
|
||||||
@@ -19,7 +19,7 @@ export class DerivedBvh {
|
|||||||
pick(origin, direction, maxDistance = Infinity, maxHits = 1) {
|
pick(origin, direction, maxDistance = Infinity, maxHits = 1) {
|
||||||
if (this.root < 0) return []; let magnitude = Math.hypot(...direction); const dir = direction.map(v => v / magnitude);
|
if (this.root < 0) return []; let magnitude = Math.hypot(...direction); const dir = direction.map(v => v / magnitude);
|
||||||
const intersect = (array, at) => { let lo = 0, hi = maxDistance; for (let a = 0; a < 3; a++) { const min = array[at + a], max = array[at + a + 3]; if (dir[a] === 0) { if (origin[a] < min || origin[a] > max) return Infinity; } else { let x = (min - origin[a]) / dir[a], y = (max - origin[a]) / dir[a]; if (x > y) [x, y] = [y, x]; lo = Math.max(lo, x); hi = Math.min(hi, y); if (lo > hi) return Infinity; } } return lo; };
|
const intersect = (array, at) => { let lo = 0, hi = maxDistance; for (let a = 0; a < 3; a++) { const min = array[at + a], max = array[at + a + 3]; if (dir[a] === 0) { if (origin[a] < min || origin[a] > max) return Infinity; } else { let x = (min - origin[a]) / dir[a], y = (max - origin[a]) / dir[a]; if (x > y) [x, y] = [y, x]; lo = Math.max(lo, x); hi = Math.min(hi, y); if (lo > hi) return Infinity; } } return lo; };
|
||||||
const hits = [], stack = [this.root]; while (stack.length) { const n = stack.pop(); if (intersect(this.nodeBounds, n * 6) === Infinity) continue; if (this.leafCount[n]) for (let j = 0; j < this.leafCount[n]; j++) { const i = this.leaves[this.leafStart[n] + j]; if (!this.pickable[i]) continue; const distance = intersect(this.bounds, i * 6); if (distance !== Infinity) hits.push({slot: this.identity[i * 2], generation: this.identity[i * 2 + 1], distance}); } else stack.push(this.right[n], this.left[n]); }
|
const hits = [], stack = [this.root]; while (stack.length) { const n = stack.pop(); if (intersect(this.nodeBounds, n * 6) === Infinity) continue; if (this.leafCount[n]) for (let j = 0; j < this.leafCount[n]; j++) { const i = this.leaves[this.leafStart[n] + j]; const distance = intersect(this.bounds, i * 6); if (distance !== Infinity) hits.push({slot: this.identity[i * 2], generation: this.identity[i * 2 + 1], distance}); } else stack.push(this.right[n], this.left[n]); }
|
||||||
hits.sort((a, b) => a.distance - b.distance || a.slot - b.slot || a.generation - b.generation); return hits.slice(0, maxHits);
|
hits.sort((a, b) => a.distance - b.distance || a.slot - b.slot || a.generation - b.generation); return hits.slice(0, maxHits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-17
@@ -1,8 +1,7 @@
|
|||||||
import { SnapshotReader } from "./render-data-snapshot.js";
|
import { SnapshotReader } from "./render-data-snapshot.js";
|
||||||
|
|
||||||
export const VISIBLE = 1;
|
|
||||||
const HEADER_WORDS = 16, SLOT_WORDS = 40, CAPACITY = 1024, SLOT_VERSION = 2;
|
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 OP = { IMPORT_GLB: 1, CREATE_INSTANCE: 3, 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");
|
const HANDLE_TOKEN = Symbol("renderer handle");
|
||||||
|
|
||||||
export class RendererError extends Error {
|
export class RendererError extends Error {
|
||||||
@@ -157,11 +156,11 @@ export class RendererClient {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mesh(handle, defaultType = Array(16).fill(0)) {
|
#mesh(handle, defaultInstanceHandle, defaultType = Array(16).fill(0)) {
|
||||||
return new Mesh(HANDLE_TOKEN,
|
return new Mesh(HANDLE_TOKEN,
|
||||||
visible => { validateVisible(visible); return this.#enqueue(OP.MESH_FLAGS, [...handle, visible ? 1 : 0]); },
|
this.#instance(defaultInstanceHandle),
|
||||||
async (transform, {type = defaultType, visible} = {}) => {
|
async (transform, {type = defaultType} = {}) => {
|
||||||
type = typeWords(type); if (visible !== undefined) { validateVisible(visible); type[0] = (type[0] & ~1) | Number(visible); }
|
type = typeWords(type);
|
||||||
const result = await this.#enqueue(OP.CREATE_INSTANCE, [...handle, ...floatWords(transform), ...type]);
|
const result = await this.#enqueue(OP.CREATE_INSTANCE, [...handle, ...floatWords(transform), ...type]);
|
||||||
return this.#instance(result);
|
return this.#instance(result);
|
||||||
});
|
});
|
||||||
@@ -169,7 +168,6 @@ export class RendererClient {
|
|||||||
|
|
||||||
#instance(handle) {
|
#instance(handle) {
|
||||||
return new Instance(HANDLE_TOKEN,
|
return new Instance(HANDLE_TOKEN,
|
||||||
visible => { validateVisible(visible); return this.#enqueue(OP.INSTANCE_VISIBLE, [...handle, visible ? 1 : 0]); },
|
|
||||||
type => this.#enqueue(OP.SET_INSTANCE_TYPE, [...handle, ...typeWords(type)]),
|
type => this.#enqueue(OP.SET_INSTANCE_TYPE, [...handle, ...typeWords(type)]),
|
||||||
transform => this.#enqueue(OP.INSTANCE_TRANSFORM, [...handle, ...floatWords(transform)]),
|
transform => this.#enqueue(OP.INSTANCE_TRANSFORM, [...handle, ...floatWords(transform)]),
|
||||||
() => this.#enqueue(OP.DESTROY_INSTANCE, [...handle]));
|
() => this.#enqueue(OP.DESTROY_INSTANCE, [...handle]));
|
||||||
@@ -185,7 +183,7 @@ export class RendererClient {
|
|||||||
else throw new TypeError("GLB source must be URL, File, or ArrayBuffer");
|
else throw new TypeError("GLB source must be URL, File, or ArrayBuffer");
|
||||||
if (this.#disposed) throw new RendererError("DISPOSED");
|
if (this.#disposed) throw new RendererError("DISPOSED");
|
||||||
const result = await this.#withPayload(buffer, OP.IMPORT_GLB, [framing === "interior" ? 1 : 0]);
|
const result = await this.#withPayload(buffer, OP.IMPORT_GLB, [framing === "interior" ? 1 : 0]);
|
||||||
return result.meshes.map(item => this.#mesh(item.handle, item.defaultType));
|
return result.meshes.map(item => this.#mesh(item.handle, item.defaultInstance, item.defaultType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -260,7 +258,6 @@ 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");
|
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 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) {
|
function floatWords(matrix) {
|
||||||
@@ -269,27 +266,25 @@ function floatWords(matrix) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Mesh {
|
class Mesh {
|
||||||
#setVisible; #createInstance;
|
#defaultInstance; #createInstance;
|
||||||
constructor(token, setVisible, createInstance) {
|
constructor(token, defaultInstance, createInstance) {
|
||||||
if (token !== HANDLE_TOKEN) throw new TypeError("Mesh cannot be constructed directly");
|
if (token !== HANDLE_TOKEN) throw new TypeError("Mesh cannot be constructed directly");
|
||||||
this.#setVisible = setVisible;
|
this.#defaultInstance = defaultInstance;
|
||||||
this.#createInstance = createInstance;
|
this.#createInstance = createInstance;
|
||||||
}
|
}
|
||||||
setVisible(visible) { return this.#setVisible(visible); }
|
get defaultInstance() { return this.#defaultInstance; }
|
||||||
createInstance(transform, options = {}) { return this.#createInstance(transform, options); }
|
createInstance(transform, options = {}) { return this.#createInstance(transform, options); }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Instance {
|
class Instance {
|
||||||
#setVisible; #setType; #setTransform; #destroy; #dead = false;
|
#setType; #setTransform; #destroy; #dead = false;
|
||||||
constructor(token, setVisible, setType, setTransform, destroy) {
|
constructor(token, setType, setTransform, destroy) {
|
||||||
if (token !== HANDLE_TOKEN) throw new TypeError("Instance cannot be constructed directly");
|
if (token !== HANDLE_TOKEN) throw new TypeError("Instance cannot be constructed directly");
|
||||||
this.#setVisible = setVisible;
|
|
||||||
this.#setType = setType;
|
this.#setType = setType;
|
||||||
this.#setTransform = setTransform;
|
this.#setTransform = setTransform;
|
||||||
this.#destroy = destroy;
|
this.#destroy = destroy;
|
||||||
}
|
}
|
||||||
#live() { if (this.#dead) throw new RendererError("STALE_HANDLE"); }
|
#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); }
|
setType(words) { this.#live(); return this.#setType(words); }
|
||||||
setTransform(transform) { this.#live(); return this.#setTransform(transform); }
|
setTransform(transform) { this.#live(); return this.#setTransform(transform); }
|
||||||
async destroy() { this.#live(); await this.#destroy(); this.#dead = true; }
|
async destroy() { this.#live(); await this.#destroy(); this.#dead = true; }
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ test("all presets use current schemas, versions, and one frame output", () => {
|
|||||||
.parameters.texture.sampleCount, "number");
|
.parameters.texture.sampleCount, "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("presets classify visibility and material through type.words[0] predicates", () => {
|
test("presets classify demo-owned enable and material bits through type.words[0] predicates", () => {
|
||||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||||
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
||||||
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" }, name);
|
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" }, name);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import test from "node:test";
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import * as rendererModule from "../static/renderer-client.js";
|
import * as rendererModule from "../static/renderer-client.js";
|
||||||
const { RendererClient, RendererError } = rendererModule;
|
const { RendererClient, RendererError } = rendererModule;
|
||||||
|
const TYPE = [0,1,2,4,8,16,32,64,128,256,512,1024,2048,4096,0x80000000,0xffffffff];
|
||||||
|
|
||||||
class WorkerMock extends EventTarget {
|
class WorkerMock extends EventTarget {
|
||||||
messages=[]; transfers=[]; terminated=false;
|
messages=[]; transfers=[]; terminated=false;
|
||||||
@@ -22,26 +23,30 @@ async function imported(f) {
|
|||||||
const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
|
const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
|
||||||
f.worker.reply({type:"payload-ready",id:1});
|
f.worker.reply({type:"payload-ready",id:1});
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
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]}]}});
|
f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[{handle:[7,3],defaultInstance:[8,5],defaultType:[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}});
|
||||||
return (await loading)[0];
|
return (await loading)[0];
|
||||||
}
|
}
|
||||||
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("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("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 () => {
|
test("writes tagged fixed-slot protocol and resolves reply", async () => {
|
||||||
const f=fixture(); const mesh=await imported(f);
|
const f=fixture(); const mesh=await imported(f);
|
||||||
const pending=mesh.setVisible(true);
|
assert.equal(mesh.setVisible,undefined);
|
||||||
|
assert.equal(mesh.setType,undefined);
|
||||||
|
assert.equal(typeof mesh.defaultInstance.setType,"function");
|
||||||
|
const pending=mesh.defaultInstance.setType(TYPE);
|
||||||
const {memory,header,worker}=f;
|
const {memory,header,worker}=f;
|
||||||
assert.equal(Atomics.load(header,5),2);
|
assert.equal(Atomics.load(header,5),2);
|
||||||
const slot=new Int32Array(memory.buffer,64+160,40);
|
const slot=new Int32Array(memory.buffer,64+160,40);
|
||||||
assert.deepEqual([...slot.slice(0,6)],[2,2,2,7,3,1]);
|
assert.deepEqual([...slot.slice(0,21)].map(x=>x>>>0),[2,10,2,8,5,...TYPE]);
|
||||||
worker.reply({type:"reply",request:2,ok:true,code:"OK"}); await pending;
|
worker.reply({type:"reply",request:2,ok:true,code:"OK"}); await pending;
|
||||||
});
|
});
|
||||||
test("maps stable errors and gates destroyed instances", async () => {
|
test("maps stable errors and gates destroyed instances", async () => {
|
||||||
const f=fixture(), mesh=await imported(f); const {worker}=f;
|
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]),{visible:false});
|
const creating=mesh.createInstance(new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),{type:TYPE});
|
||||||
worker.reply({type:"reply",request:2,ok:true,result:[4,2]}); const instance=await creating;
|
worker.reply({type:"reply",request:2,ok:true,result:[4,2]}); const instance=await creating;
|
||||||
|
assert.equal(instance.setVisible,undefined);
|
||||||
const destroying=instance.destroy(); worker.reply({type:"reply",request:3,ok:true}); await destroying;
|
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");
|
assert.throws(()=>instance.setType(TYPE), error=>error instanceof RendererError&&error.code==="STALE_HANDLE");
|
||||||
});
|
});
|
||||||
test("rejects protocol mismatch", () => {
|
test("rejects protocol mismatch", () => {
|
||||||
const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=1;
|
const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=1;
|
||||||
@@ -49,7 +54,7 @@ test("rejects protocol mismatch", () => {
|
|||||||
});
|
});
|
||||||
test("pending reply exists before ring publication", async () => {
|
test("pending reply exists before ring publication", async () => {
|
||||||
const f=fixture(), mesh=await imported(f); const {worker}=f;
|
const f=fixture(), mesh=await imported(f); const {worker}=f;
|
||||||
const pending=mesh.setVisible(true);
|
const pending=mesh.defaultInstance.setType(TYPE);
|
||||||
worker.reply({type:"reply",request:2,ok:true});
|
worker.reply({type:"reply",request:2,ok:true});
|
||||||
await pending;
|
await pending;
|
||||||
});
|
});
|
||||||
@@ -61,7 +66,7 @@ test("profile snapshots have a dedicated getter", () => {
|
|||||||
});
|
});
|
||||||
test("worker failures and dispose reject every pending operation", async () => {
|
test("worker failures and dispose reject every pending operation", async () => {
|
||||||
const f=fixture(), mesh=await imported(f); const {worker,client,bridge}=f;
|
const f=fixture(), mesh=await imported(f); const {worker,client,bridge}=f;
|
||||||
const a=mesh.setVisible(true), b=mesh.setVisible(false);
|
const a=mesh.defaultInstance.setType(TYPE), b=mesh.defaultInstance.setType([...TYPE].reverse());
|
||||||
worker.dispatchEvent(new Event("error"));
|
worker.dispatchEvent(new Event("error"));
|
||||||
await assert.rejects(a,/WORKER_ERROR/); await assert.rejects(b,/WORKER_ERROR/);
|
await assert.rejects(a,/WORKER_ERROR/); await assert.rejects(b,/WORKER_ERROR/);
|
||||||
client.dispose(); assert.equal(worker.terminated,true); assert.equal(bridge.freed,true);
|
client.dispose(); assert.equal(worker.terminated,true); assert.equal(bridge.freed,true);
|
||||||
@@ -75,6 +80,7 @@ test("import always releases staged payload when ring is full", async () => {
|
|||||||
});
|
});
|
||||||
test("does not export handle constructors or internal mutation methods", () => {
|
test("does not export handle constructors or internal mutation methods", () => {
|
||||||
const {client}=fixture();
|
const {client}=fixture();
|
||||||
|
assert.equal(rendererModule.VISIBLE,undefined);
|
||||||
assert.equal(rendererModule.Mesh,undefined);
|
assert.equal(rendererModule.Mesh,undefined);
|
||||||
assert.equal(rendererModule.Instance,undefined);
|
assert.equal(rendererModule.Instance,undefined);
|
||||||
assert.equal(client._meshFlags,undefined);
|
assert.equal(client._meshFlags,undefined);
|
||||||
|
|||||||
@@ -79,26 +79,26 @@ test("snapshot reader refreshes memory after pinning", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function bvhSnapshot({ pickable = [1, 1], shifted = false } = {}) {
|
function bvhSnapshot({ metadata = [0, 0x80000000], shifted = false } = {}) {
|
||||||
const count = 2;
|
const count = 2;
|
||||||
return { instanceCount: count, streams: {
|
return { instanceCount: count, streams: {
|
||||||
instanceSlot: Uint32Array.from([5, 6]), instanceGeneration: Uint32Array.from([1, 1]),
|
instanceSlot: Uint32Array.from([5, 6]), instanceGeneration: Uint32Array.from([1, 1]),
|
||||||
instanceMeshSlot: Uint32Array.from([2, 2]), instanceMeshGeneration: Uint32Array.from([4, 4]),
|
instanceMeshSlot: Uint32Array.from([2, 2]), instanceMeshGeneration: Uint32Array.from([4, 4]),
|
||||||
instanceType: Uint32Array.from(pickable.flatMap(x => [x, ...Array(15).fill(0)])),
|
instanceType: Uint32Array.from(metadata.flatMap(x => [x, ...Array(15).fill(0)])),
|
||||||
instanceWorldMin: Float32Array.from(shifted ? [10, -1, -1, 4, -1, -1] : [2, -1, -1, 4, -1, -1]),
|
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]),
|
instanceWorldMax: Float32Array.from(shifted ? [12, 1, 1, 6, 1, 1] : [3, 1, 1, 6, 1, 1]),
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("BVH preserves topology for visibility/refit and reports world distances", () => {
|
test("BVH ignores opaque instance metadata during refit and picking", () => {
|
||||||
const bvh = new DerivedBvh();
|
const bvh = new DerivedBvh();
|
||||||
bvh.update(bvhSnapshot());
|
bvh.update(bvhSnapshot());
|
||||||
assert.equal(bvh.rebuilds, 1);
|
assert.equal(bvh.rebuilds, 1);
|
||||||
assert.equal(bvh.pick([0, 0, 0], [2, 0, 0], Infinity, 2)[0].distance, 2);
|
assert.equal(bvh.pick([0, 0, 0], [2, 0, 0], Infinity, 2)[0].distance, 2);
|
||||||
bvh.update(bvhSnapshot({ pickable: [0, 1], shifted: true }));
|
bvh.update(bvhSnapshot({ metadata: [0xffffffff, 0], shifted: true }));
|
||||||
assert.equal(bvh.rebuilds, 1);
|
assert.equal(bvh.rebuilds, 1);
|
||||||
assert.equal(bvh.refits, 1);
|
assert.equal(bvh.refits, 1);
|
||||||
assert.deepEqual(bvh.pick([0, 0, 0], [1, 0, 0], Infinity, 2).map(hit => hit.slot), [6]);
|
assert.deepEqual(bvh.pick([0, 0, 0], [1, 0, 0], Infinity, 2).map(hit => hit.slot), [6, 5]);
|
||||||
});
|
});
|
||||||
|
|
||||||
class WorkerMock extends EventTarget {
|
class WorkerMock extends EventTarget {
|
||||||
@@ -108,7 +108,7 @@ class WorkerMock extends EventTarget {
|
|||||||
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
|
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
|
||||||
}
|
}
|
||||||
|
|
||||||
test("renderer pick returns gated instances and exact epoch", async () => {
|
test("renderer pick returns instance metadata handles and exact epoch", async () => {
|
||||||
const scene = snapshotFixture();
|
const scene = snapshotFixture();
|
||||||
const ring = 8192;
|
const ring = 8192;
|
||||||
const ringHeader = new Int32Array(scene.memory.buffer, ring, 16);
|
const ringHeader = new Int32Array(scene.memory.buffer, ring, 16);
|
||||||
@@ -124,7 +124,8 @@ test("renderer pick returns gated instances and exact epoch", async () => {
|
|||||||
const result = await picking;
|
const result = await picking;
|
||||||
assert.equal(result.epoch, 1);
|
assert.equal(result.epoch, 1);
|
||||||
assert.equal(result.hits[0].distance, 2);
|
assert.equal(result.hits[0].distance, 2);
|
||||||
assert.equal(typeof result.hits[0].instance.setVisible, "function");
|
assert.equal(typeof result.hits[0].instance.setType, "function");
|
||||||
|
assert.equal(result.hits[0].instance.setVisible, undefined);
|
||||||
client.dispose();
|
client.dispose();
|
||||||
assert.equal(bvhWorker.terminated, true);
|
assert.equal(bvhWorker.terminated, true);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user