ifcviewer: pack the cull-hot instance fields and skip unchanged culls

On the single-threaded web build the CPU cull WAS the frame: 52-60 ms
of a 60 ms frame at 640k instances (desktop hides the same cost across
cores via std::async, which web cannot use without COOP/COEP+pthreads).

Two changes, both also helping desktop:

- ModelGpuData::CullInstance packs the six AABB floats and three ids the
  cull reads into 40 contiguous bytes. InstanceInfo is 232 bytes with
  the AABB 200 bytes away from the ids, so the walk paid two or three
  cache lines per instance. Rebuilt by rebuildCullInstances at model
  apply and inside uploadInstanceRecords, which every recompose,
  transform and colour-override change already funnels through.
  Measured on web: 94 ns/instance -> 36 ns/instance during a continuous
  orbit (~2.6x).

- render() re-culls only when a cull input changed: the camera, a
  cull-relevant setting (contribution px, LOD px, x-ray, HiZ on/off), a
  fresh HiZ pyramid, or scene_epoch_ — bumped by chunk residency,
  visibility, colours, transforms, model add/remove/hide/unload. A
  frame requested for an overlay redraw, pick feedback, or a streaming
  tick where nothing landed draws from the buffers the last cull
  uploaded and skips the walk entirely. Benchmarks are exempt so bench
  numbers keep measuring the real cull.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-24 11:18:56 +10:00
parent 73f8e6aea7
commit 4a761b51f5
3 changed files with 124 additions and 15 deletions
+21
View File
@@ -395,6 +395,25 @@ struct ModelGpuData {
std::vector<MeshInfo> meshes;
std::vector<InstanceInfo> instances;
// The cull-hot per-instance fields packed contiguously. InstanceInfo is
// 232 bytes with the AABB 200 bytes from the ids, so the per-frame cull
// paid two or three cache lines per instance — at half a million
// instances that is the whole frame budget on the single-threaded web
// build. 40 bytes per entry here makes the walk sequential. Rebuilt by
// rebuildCullInstances wherever instances change (applyCachedModel,
// uploadInstanceRecords — which every recompose and colour change
// already funnels through).
struct CullInstance {
float aabb_min[3];
float aabb_max[3];
std::uint32_t mesh_id;
std::uint32_t object_id;
std::uint32_t color_override_rgba8;
std::uint32_t chunk_idx;
};
static_assert(sizeof(CullInstance) == 40, "keep the cull walk dense");
std::vector<CullInstance> cull_instances;
// Per-mesh "any vertex has alpha < 255?" flag, indexed by mesh_id.
// Populated at uploadStreamedMesh / applyStreamedChunk as vertex bytes
// become CPU-resident. Used at cull time to classify each instance
@@ -501,5 +520,7 @@ void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool);
// and the per-chunk cull buffers. Chunk bookkeeping is left intact so the
// buffers can be re-created — the undo step of a failed model load.
void releaseModelBuffers(ModelGpuData& m);
// Refresh ModelGpuData::cull_instances from instances + instance_chunk_idx.
void rebuildCullInstances(ModelGpuData& m);
#endif // WGPUMODELGPUDATA_H