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 committed 2026-07-27 01:11:21 +03:00
1 parent b5f9a06e78
commit 4887a53d66
8 files changed
+1747 -400

No files matched your search

+50 -3
View File
@@ -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
View 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);
}