perf(render): batch and cull mesh instances

Share repeated block geometry across GPU instances.\n\nPatch changed mesh chunks and cull large batches with indirect draws.
This commit is contained in:
Hakan Seven 2026-07-27 01:11:21 +03:00
commit 4887a53d66
8 changed files with 1744 additions and 397 deletions

View file

@ -557,10 +557,11 @@ impl Scene {
})
})
.collect();
for (handle, mesh, top_level) in built {
for (handle, mut mesh, top_level) in built {
if top_level {
self.meshes.insert(handle, mesh);
} else {
mesh.prepare_instance_source(handle);
self.block_meshes.insert(handle, mesh);
}
}
@ -2232,10 +2233,11 @@ impl Scene {
})
})
.collect();
for (handle, m, top_level) in built {
for (handle, mut m, top_level) in built {
if top_level {
self.meshes.insert(handle, m);
} else {
m.prepare_instance_source(handle);
self.block_meshes.insert(handle, m);
}
}

View file

@ -651,10 +651,11 @@ fn build_derived_caches_impl(
.collect();
let mut meshes: HashMap<Handle, MeshLodSet> = HashMap::default();
let mut block_meshes: HashMap<Handle, MeshLodSet> = HashMap::default();
for (handle, m, top_level) in built {
for (handle, mut m, top_level) in built {
if top_level {
meshes.insert(handle, m);
} else {
m.prepare_instance_source(handle);
block_meshes.insert(handle, m);
}
}
@ -1037,6 +1038,7 @@ fn transform_block_mesh_lod_set(
) -> MeshLodSet {
use acadrust::types::Vector3;
let mut out = set.clone();
out.instance_transform = Some(*xform);
let transform_direction = |direction: [f32; 3]| {
let transformed = xform.apply_rotation(Vector3::new(
direction[0] as f64,

View file

@ -167,6 +167,19 @@ pub struct MeshLodSet {
/// triangle). `verts` carry only the high half of the double-single
/// position, so the bound is f32-precise — fine for a conservative cull.
pub z_aabb: [f32; 2],
/// Immutable block-local geometry shared by every INSERT instance of this
/// block entity. Top-level meshes leave this empty.
pub instance_source: Option<std::sync::Arc<MeshInstanceSource>>,
/// Accumulated block-local → world transform for this rendered instance.
pub instance_transform: Option<acadrust::types::Transform>,
}
#[derive(Clone, Debug)]
pub struct MeshInstanceSource {
pub handle: acadrust::Handle,
pub lods: Vec<MeshModel>,
pub edge_verts: Vec<[f32; 3]>,
pub edge_verts_low: Vec<[f32; 3]>,
}
/// 3D bounds of every LOD's vertices: `([min_x, min_y, max_x, max_y], [min_z, max_z])`.
@ -279,6 +292,8 @@ impl MeshLodSet {
metrics,
world_aabb,
z_aabb,
instance_source: None,
instance_transform: None,
}
}
@ -297,4 +312,14 @@ impl MeshLodSet {
self.world_aabb = xy;
self.z_aabb = z;
}
pub fn prepare_instance_source(&mut self, handle: acadrust::Handle) {
self.instance_source = Some(std::sync::Arc::new(MeshInstanceSource {
handle,
lods: self.lods.clone(),
edge_verts: self.edge_verts.clone(),
edge_verts_low: self.edge_verts_low.clone(),
}));
self.instance_transform = None;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -262,7 +262,7 @@ impl shader::Primitive for Primitive {
inner.cached_epoch = (u64::MAX, u64::MAX, u64::MAX);
inner.cached_wire_id = u64::MAX;
inner.cached_selection = (u64::MAX, u64::MAX);
inner.cached_mesh_key = (u64::MAX, u64::MAX);
inner.cached_mesh_content_id = u64::MAX;
inner.cached_face3d_key = (u64::MAX, false);
inner.cached_hatch_source = None;
inner.cached_wipeout_source = None;
@ -768,10 +768,21 @@ impl shader::Primitive for Primitive {
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.meshes))
{
inner.upload_mesh_batch(device, queue, &vp.meshes[..]);
let patched = vp.wire_patch.as_ref().is_some_and(|(previous, patch)| {
*previous == inner.cached_mesh_content_id
&& inner.patch_mesh_batch(
device,
queue,
&vp.meshes[..],
&patch.changes,
)
});
if !patched {
inner.upload_mesh_batch(device, queue, &vp.meshes[..]);
}
inner.cached_mesh_source = Some(Arc::clone(&vp.meshes));
inner.cached_mesh_batch_epoch = vp.geometry_epoch;
}
inner.cached_mesh_content_id = vp.wire_content_id;
// Selection / hover highlight overlay — tinted copies of just the
// picked solids, rebuilt only when the highlight set (or geometry)
// changes. Drawn over the static batch so the base never re-packs.
@ -780,12 +791,7 @@ impl shader::Primitive for Primitive {
vp.selection_generation,
);
if hl_key != inner.cached_highlight_key {
inner.upload_mesh_highlight(
device,
&vp.meshes[..],
&vp.selected_handles,
vp.hover_handle,
);
inner.update_mesh_highlight(&vp.selected_handles, vp.hover_handle);
inner.cached_highlight_key = hl_key;
}
// Live overlay (command preview / interim / grip drag) — small and
@ -812,7 +818,13 @@ impl shader::Primitive for Primitive {
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_mesh_lod(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_mesh_lod(
queue,
view_rot,
eye,
clip_size.width,
clip_size.height,
);
#[cfg(not(target_arch = "wasm32"))]
{
let cull_key = (