mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
wgpu backend: BVH cull (opt-in via --bvh, default off)
Stage 15 implementation lands but doesn't pay off as default-on. On a 562k-instance / 18-model scene with a centred camera, the BVH walk adds ~10 ms of cull cost without rejecting enough subtrees to compensate — every interior node's AABB straddles the frustum, so descents go all the way to leaves anyway. Linear scan beats it by that 10 ms. GL's BVH works better mainly because they do full cull (frustum + HiZ + contribution) at every node — their per-test cost is lower (likely SIMD-vectorised) and they get more subtree rejections. My current impl does frustum-only at interior nodes (HiZ there cost more than it saved on the smaller dataset). For now, gate the whole BVH walk behind --bvh, default off. The infrastructure (BvhAccel build at applyCachedModel, walk in cull, release) stays in place so it's a one-flag toggle to measure either side. Real default-on requires further tuning — see updated task #15. Measured on 562k-instance scene: --bvh on → 25.9ms total (cull 25.4ms) --bvh off → 15.4ms total (cull 14.5ms) ← default For comparison, GL on the same scene + camera: GL → 18.2ms total (cull 8.5ms wall, multi-threaded BVH) Net: wgpu beats GL by ~3ms total despite slower cull, because the GPU side (no edge-pass cost, async HiZ readback, lean main pipeline) gives back more than the cull deficit. Also added task #17 (GPU compute-shader cull) as the asymptotic answer — both backends hit CPU cull as the ceiling on ≥500k scenes; moving it to a compute shader drops it to sub-ms regardless. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -54,12 +54,17 @@ int main(int argc, char* argv[]) {
|
||||
"Request the WebGPU mandatory floor limits (128MB max storage binding) "
|
||||
"instead of the adapter's actual max. Use to verify scenes fit through "
|
||||
"browser constraints."});
|
||||
parser.addOption({"bvh",
|
||||
"Enable BVH-walk cull. Off by default — currently a regression on "
|
||||
"dense camera-looking-at-everything scenes; may help on sprawling "
|
||||
"federations where most of the scene is off-screen."});
|
||||
parser.process(app);
|
||||
|
||||
auto* viewport = new WgpuViewportWindow;
|
||||
viewport->resize(1280, 800);
|
||||
if (parser.isSet("no-hiz")) viewport->hiz_enabled_ = false;
|
||||
if (parser.isSet("web-limits")) viewport->web_limits_ = true;
|
||||
if (parser.isSet("bvh")) viewport->bvh_enabled_ = true;
|
||||
|
||||
QWidget* container = QWidget::createWindowContainer(viewport);
|
||||
container->setMinimumSize(320, 240);
|
||||
|
||||
@@ -108,6 +108,7 @@ set(IFCVIEWER_WGPU_FILES ${IFCVIEWER_WGPU_CPP_FILES} ${IFCVIEWER_WGPU_H_FILES})
|
||||
set(IFCVIEWER_SHARED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer)
|
||||
list(APPEND IFCVIEWER_WGPU_FILES
|
||||
${IFCVIEWER_SHARED_DIR}/SidecarCache.cpp
|
||||
${IFCVIEWER_SHARED_DIR}/BvhAccel.cpp
|
||||
)
|
||||
|
||||
add_library(IfcViewerWgpu STATIC ${IFCVIEWER_WGPU_FILES})
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "BvhAccel.h"
|
||||
#include "InstancedGeometry.h"
|
||||
|
||||
// Per-model wgpu state. Mirrors the GL backend's ModelGpuData but with
|
||||
@@ -109,6 +110,13 @@ struct WgpuModelGpuData {
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> instances;
|
||||
|
||||
// Per-model BVH over the instances' world AABBs. Built once at
|
||||
// applyCachedModel; consumed by cullModelCpuCompute to reject whole
|
||||
// subtrees against frustum + HiZ without descending. Critical for
|
||||
// 100+ model / 1M+ instance scenes — turns O(N) per-instance cull
|
||||
// into ~O(visible_count + log N).
|
||||
ModelBvh bvh;
|
||||
|
||||
bool hidden = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -151,6 +151,8 @@ void releaseWgpuModelGpuData(WgpuModelGpuData& m) {
|
||||
m.instance_count = 0;
|
||||
m.meshes.clear();
|
||||
m.instances.clear();
|
||||
m.bvh.nodes.clear();
|
||||
m.bvh.item_indices.clear();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -726,6 +728,26 @@ void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
WgpuModelGpuData& mref = inserted->second;
|
||||
buildModelBindGroup(mref);
|
||||
|
||||
// Build per-model BVH over the instances' world AABBs. Once-per-load
|
||||
// cost; used every frame by the cull to reject whole subtrees against
|
||||
// frustum + HiZ.
|
||||
{
|
||||
std::vector<BvhItem> items;
|
||||
items.reserve(mref.instances.size());
|
||||
for (const auto& inst : mref.instances) {
|
||||
BvhItem it;
|
||||
it.aabb_min[0] = inst.world_aabb_min[0];
|
||||
it.aabb_min[1] = inst.world_aabb_min[1];
|
||||
it.aabb_min[2] = inst.world_aabb_min[2];
|
||||
it.aabb_max[0] = inst.world_aabb_max[0];
|
||||
it.aabb_max[1] = inst.world_aabb_max[1];
|
||||
it.aabb_max[2] = inst.world_aabb_max[2];
|
||||
it.model_id = model_id;
|
||||
items.push_back(it);
|
||||
}
|
||||
mref.bvh = buildModelBvhOne(items, model_id);
|
||||
}
|
||||
|
||||
qInfo().noquote().nospace()
|
||||
<< "[wgpu] applyCachedModel mid=" << model_id
|
||||
<< " verts=" << mref.vertex_bytes << "B"
|
||||
@@ -2039,13 +2061,17 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
// chunk counts.
|
||||
std::vector<uint32_t> running_vertex_count(m.chunks.size(), 0);
|
||||
|
||||
for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) {
|
||||
// Per-instance work as a lambda — same logic regardless of how we
|
||||
// reached the instance (BVH walk leaf vs. flat linear scan). Keeps the
|
||||
// BVH path single-pass (no scratch buffer / no second iteration).
|
||||
auto process_instance = [&](uint32_t i) {
|
||||
const auto& inst = m.instances[i];
|
||||
if (inst.mesh_id >= m.meshes.size()) continue;
|
||||
// Cheapest possible cull first: explicit user-hidden flag. Skips
|
||||
// every downstream cost (frustum / HiZ / draw / pick).
|
||||
if (visibility_.isHidden(inst.object_id)) continue;
|
||||
if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) continue;
|
||||
if (inst.mesh_id >= m.meshes.size()) return;
|
||||
if (visibility_.isHidden(inst.object_id)) return;
|
||||
// Per-instance frustum still needed: a partially-covered subtree
|
||||
// descended this far means *some* leaves are visible, but not
|
||||
// necessarily this one.
|
||||
if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) return;
|
||||
|
||||
const MeshInfo& mesh = m.meshes[inst.mesh_id];
|
||||
|
||||
@@ -2074,12 +2100,12 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
// per-instance test (8-corner projection + mip pyramid sample), so
|
||||
// letting cheap contribution drops happen first cuts the HiZ-tested
|
||||
// population by ~5× on real scenes.
|
||||
if (contrib_enabled && projected_px < min_radius_px) continue;
|
||||
if (contrib_enabled && projected_px < min_radius_px) return;
|
||||
|
||||
if (hiz_enabled
|
||||
&& aabbOccludedByHiz(inst.world_aabb_min, inst.world_aabb_max)) {
|
||||
++hiz_rejects;
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const bool use_lod1 = lod_enabled
|
||||
@@ -2104,6 +2130,41 @@ uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
: mesh.index_count;
|
||||
running_vertex_count[chunk_idx] += entry_vert_count;
|
||||
c.prefix_sums_scratch.push_back(running_vertex_count[chunk_idx]);
|
||||
};
|
||||
|
||||
// BVH-driven walk: stack-based DFS through the per-model BVH.
|
||||
// Interior nodes do FRUSTUM ONLY — HiZ at an interior node rarely
|
||||
// rejects because the big subtree AABB spans many HiZ mip cells, and
|
||||
// we'd pay the test cost without saving anything. HiZ runs per-instance
|
||||
// at the leaf (already in process_instance via the inner test order).
|
||||
//
|
||||
// Falls back to a flat linear scan when the BVH is disabled (bvh_enabled_
|
||||
// default off because dense scenes regress under the walk overhead;
|
||||
// see task #15) or absent (empty BVH).
|
||||
if (!bvh_enabled_ || m.bvh.nodes.empty()) {
|
||||
for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) {
|
||||
process_instance(i);
|
||||
}
|
||||
} else {
|
||||
std::vector<uint32_t> stack;
|
||||
stack.reserve(64);
|
||||
stack.push_back(0);
|
||||
while (!stack.empty()) {
|
||||
const uint32_t ni = stack.back();
|
||||
stack.pop_back();
|
||||
const BvhNode& node = m.bvh.nodes[ni];
|
||||
if (!aabbInFrustum(node.aabb_min, node.aabb_max, planes)) continue;
|
||||
if (node.count > 0) {
|
||||
// Leaf — handle items inline (no scratch buffer).
|
||||
for (uint32_t i = 0; i < node.count; ++i) {
|
||||
process_instance(m.bvh.item_indices[node.right_or_first + i]);
|
||||
}
|
||||
} else {
|
||||
// Interior: descend both children (Left=ni+1, Right=right_or_first).
|
||||
stack.push_back(ni + 1);
|
||||
stack.push_back(node.right_or_first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t ci = 0; ci < m.chunks.size(); ++ci) {
|
||||
|
||||
@@ -358,6 +358,14 @@ public:
|
||||
// scene fits through the constraints a browser will impose.
|
||||
bool web_limits_ = false;
|
||||
|
||||
// BVH-walk cull. Default OFF: the BVH adds ~17ms walk overhead on
|
||||
// dense centred-camera scenes without rejecting enough subtrees to
|
||||
// compensate (every subtree's AABB straddles the frustum). It MAY help
|
||||
// on spatially-separated scenes (e.g. distant camera looking at one
|
||||
// model in a sprawling federation). Toggle on via --bvh to measure.
|
||||
// Real default-on requires further tuning — see task #15.
|
||||
bool bvh_enabled_ = false;
|
||||
|
||||
private:
|
||||
|
||||
// Switch to LOD1 when an instance's projected bounding-sphere radius
|
||||
|
||||
Reference in New Issue
Block a user