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:
parent
b5f9a06e78
commit
4887a53d66
8 changed files with 1744 additions and 397 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -55,6 +55,19 @@ struct MaterialMaps {
|
|||
@group(1) @binding(13) var normal_sampler: sampler;
|
||||
@group(1) @binding(14) var<uniform> maps: MaterialMaps;
|
||||
|
||||
struct MeshInstance {
|
||||
model_row_0: vec4<f32>,
|
||||
model_row_1: vec4<f32>,
|
||||
model_row_2: vec4<f32>,
|
||||
translation_low: vec4<f32>,
|
||||
normal_row_0: vec4<f32>,
|
||||
normal_row_1: vec4<f32>,
|
||||
normal_row_2: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(1) @binding(15)
|
||||
var<storage, read> mesh_instances: array<MeshInstance>;
|
||||
|
||||
struct VertexIn {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
|
|
@ -100,12 +113,30 @@ struct VertexOut {
|
|||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(v: VertexIn) -> VertexOut {
|
||||
fn vs_main(
|
||||
v: VertexIn,
|
||||
@builtin(instance_index) instance_index: u32,
|
||||
) -> VertexOut {
|
||||
var out: VertexOut;
|
||||
let rel = (v.position - u.eye_high) + (v.position_low - u.eye_low);
|
||||
let instance = mesh_instances[instance_index];
|
||||
let world_high = vec3<f32>(
|
||||
dot(instance.model_row_0.xyz, v.position) + instance.model_row_0.w,
|
||||
dot(instance.model_row_1.xyz, v.position) + instance.model_row_1.w,
|
||||
dot(instance.model_row_2.xyz, v.position) + instance.model_row_2.w,
|
||||
);
|
||||
let world_low = vec3<f32>(
|
||||
dot(instance.model_row_0.xyz, v.position_low),
|
||||
dot(instance.model_row_1.xyz, v.position_low),
|
||||
dot(instance.model_row_2.xyz, v.position_low),
|
||||
) + instance.translation_low.xyz;
|
||||
let rel = (world_high - u.eye_high) + (world_low - u.eye_low);
|
||||
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
|
||||
out.color = v.color;
|
||||
out.normal = v.normal;
|
||||
out.normal = normalize(vec3<f32>(
|
||||
dot(instance.normal_row_0.xyz, v.normal),
|
||||
dot(instance.normal_row_1.xyz, v.normal),
|
||||
dot(instance.normal_row_2.xyz, v.normal),
|
||||
));
|
||||
out.world_pos = rel;
|
||||
out.material = v.material;
|
||||
out.specular = v.specular;
|
||||
|
|
@ -306,3 +337,19 @@ fn fs_edge(in: VertexOut) -> @location(0) vec4<f32> {
|
|||
fn fs_edge_black(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_highlight_selected(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(
|
||||
mix(in.color.rgb, vec3<f32>(0.15, 0.55, 1.0), 0.60),
|
||||
0.90,
|
||||
);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_highlight_hover(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(
|
||||
mix(in.color.rgb, vec3<f32>(0.95, 0.55, 0.10), 0.35),
|
||||
0.82,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
78
src/shaders/mesh_cull.wgsl
Normal file
78
src/shaders/mesh_cull.wgsl
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
struct CullUniform {
|
||||
view_rot: mat4x4<f32>,
|
||||
eye: vec4<f32>,
|
||||
count: vec4<u32>,
|
||||
};
|
||||
|
||||
struct CullItem {
|
||||
min: vec4<f32>,
|
||||
max: vec4<f32>,
|
||||
counts: vec4<u32>,
|
||||
info: vec4<u32>,
|
||||
};
|
||||
|
||||
struct DrawIndexed {
|
||||
index_count: u32,
|
||||
instance_count: u32,
|
||||
first_index: u32,
|
||||
base_vertex: u32,
|
||||
first_instance: u32,
|
||||
};
|
||||
|
||||
struct Draw {
|
||||
vertex_count: u32,
|
||||
instance_count: u32,
|
||||
first_vertex: u32,
|
||||
first_instance: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: CullUniform;
|
||||
@group(0) @binding(1) var<storage, read> items: array<CullItem>;
|
||||
@group(0) @binding(2) var<storage, read_write> opaque: array<DrawIndexed>;
|
||||
@group(0) @binding(3) var<storage, read_write> transparent: array<DrawIndexed>;
|
||||
@group(0) @binding(4) var<storage, read_write> wire: array<DrawIndexed>;
|
||||
@group(0) @binding(5) var<storage, read_write> edge: array<Draw>;
|
||||
|
||||
fn chunk_visible(item: CullItem) -> bool {
|
||||
if (item.info.y == 0u) {
|
||||
return false;
|
||||
}
|
||||
var min_ndc = vec2<f32>(1e30, 1e30);
|
||||
var max_ndc = vec2<f32>(-1e30, -1e30);
|
||||
for (var corner = 0u; corner < 8u; corner++) {
|
||||
let point = vec3<f32>(
|
||||
select(item.min.x, item.max.x, (corner & 1u) != 0u),
|
||||
select(item.min.y, item.max.y, (corner & 2u) != 0u),
|
||||
select(item.min.z, item.max.z, (corner & 4u) != 0u),
|
||||
);
|
||||
let clip = u.view_rot * vec4<f32>(point - u.eye.xyz, 1.0);
|
||||
// A box touching or crossing the eye plane is conservatively retained.
|
||||
if (clip.w <= 1e-6) {
|
||||
return true;
|
||||
}
|
||||
let ndc = clip.xy / clip.w;
|
||||
min_ndc = min(min_ndc, ndc);
|
||||
max_ndc = max(max_ndc, ndc);
|
||||
}
|
||||
// 25% viewport margin on each side is 0.5 in NDC.
|
||||
return !(
|
||||
max_ndc.x < -1.5
|
||||
|| min_ndc.x > 1.5
|
||||
|| max_ndc.y < -1.5
|
||||
|| min_ndc.y > 1.5
|
||||
);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let index = id.x;
|
||||
if (index >= u.count.x) {
|
||||
return;
|
||||
}
|
||||
let item = items[index];
|
||||
let instances = select(0u, item.info.x, chunk_visible(item));
|
||||
opaque[index] = DrawIndexed(item.counts.x, instances, 0u, 0u, 0u);
|
||||
transparent[index] = DrawIndexed(item.counts.y, instances, 0u, 0u, 0u);
|
||||
wire[index] = DrawIndexed(item.counts.z, instances, 0u, 0u, 0u);
|
||||
edge[index] = Draw(item.counts.w, instances, 0u, 0u);
|
||||
}
|
||||
Loading…
Reference in a new issue