mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 02:23:34 +00:00
wgpu: drop dormant spatial-bucket prototype
Removed the WGPU_SPATIAL_BUCKETS=1 octree-style instance planner (planSpatialChunks, SpatialPlan, the env-var pair, the field, the load-time branch). Was an opt-in prototype kept in tree as a possible acceleration for "find the right instance chunk", but: - Benchmarked slower than mesh-keyed (~24-26 ms vs ~19.7 ms) — the higher chunk count's per-chunk bind-group + draw overhead more than ate the tight-AABB win on this dataset. - Not load-bearing for the brace correctness fix — that turned out to be the AABB-projected screen-rect priority metric (commit 6200ab9fa), which works on either chunk topology. - The "find the right chunk" hypothesis is moot: instance_chunk_idx[] is precomputed at load time, so cull has nothing to look up. Git log preserves the implementation if it's ever revisited. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -685,30 +685,13 @@ void WgpuViewportWindow::applyCachedModelStreaming(uint32_t model_id,
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk planning. Default is mesh-keyed Morton-sort + greedy pack
|
||||
// (each mesh in exactly one chunk). WGPU_SPATIAL_BUCKETS=1 swaps to
|
||||
// octree-style instance bucketing (a mesh may appear in multiple
|
||||
// chunks if its instances scatter — its vertex/index data gets
|
||||
// duplicated across those chunks' pool slices). The downstream
|
||||
// chunk-construction loop is identical either way; both branches
|
||||
// produce chunk_mesh_ids (per-chunk mesh list) and
|
||||
// instance_to_chunk (per-instance bucket index).
|
||||
// Chunk planning: sort meshes by 3D Morton code over centroids, then
|
||||
// greedy-pack into chunks ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT. Each mesh
|
||||
// ends up in exactly one chunk.
|
||||
std::vector<std::vector<uint32_t>> chunk_mesh_ids;
|
||||
std::vector<uint32_t> instance_to_chunk;
|
||||
instance_to_chunk.assign(metadata.meta.instances.size(), 0);
|
||||
|
||||
if (spatial_buckets_enabled_) {
|
||||
SpatialPlan plan = planSpatialChunks(metadata.meta.instances,
|
||||
metadata.meta.meshes);
|
||||
chunk_mesh_ids = std::move(plan.chunk_mesh_ids);
|
||||
instance_to_chunk = std::move(plan.instance_to_chunk);
|
||||
qInfo().noquote().nospace()
|
||||
<< "[wgpu spatial] mid=" << model_id << " produced "
|
||||
<< chunk_mesh_ids.size() << " spatial buckets from "
|
||||
<< metadata.meta.instances.size() << " instances";
|
||||
} else {
|
||||
// Mesh-keyed: sort meshes by 3D Morton code over centroids, then
|
||||
// greedy-pack into chunks ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT.
|
||||
{
|
||||
std::vector<uint32_t> sorted_mesh_ids =
|
||||
sortMeshIdsByMorton(n_meshes, mesh_cx, mesh_cy, mesh_cz, mesh_inst_count);
|
||||
chunk_mesh_ids.push_back({});
|
||||
@@ -1418,19 +1401,6 @@ bool WgpuViewportWindow::initWgpu() {
|
||||
"(disabled by default; see task #58)";
|
||||
}
|
||||
}
|
||||
if (const char* s = std::getenv("WGPU_SPATIAL_BUCKETS")) {
|
||||
spatial_buckets_enabled_ = (s[0] == '1');
|
||||
if (spatial_buckets_enabled_) {
|
||||
qInfo() << "[wgpu] WGPU_SPATIAL_BUCKETS=1 — using octree-style "
|
||||
"instance bucketing for the streaming planner";
|
||||
}
|
||||
}
|
||||
if (const char* s = std::getenv("WGPU_SPATIAL_BUCKET_MAX_INSTS")) {
|
||||
const long v = std::strtol(s, nullptr, 10);
|
||||
if (v > 0) spatial_max_instances_ = uint32_t(v);
|
||||
qInfo().noquote().nospace()
|
||||
<< "[wgpu] WGPU_SPATIAL_BUCKET_MAX_INSTS=" << spatial_max_instances_;
|
||||
}
|
||||
if (const char* s = std::getenv("WGPU_CULL_THREADS")) {
|
||||
// "0" disables std::async dispatch — every model is culled on the
|
||||
// main thread, sequentially. Used to measure speedup vs the
|
||||
@@ -5214,132 +5184,6 @@ float WgpuViewportWindow::chunkScreenAreaPx(const WgpuModelGpuData::Chunk& c,
|
||||
return (xmax - xmin) * (ymax - ymin);
|
||||
}
|
||||
|
||||
WgpuViewportWindow::SpatialPlan WgpuViewportWindow::planSpatialChunks(
|
||||
const std::vector<InstanceCpu>& instances,
|
||||
const std::vector<MeshInfo>& meshes) const {
|
||||
SpatialPlan out;
|
||||
if (instances.empty()) return out;
|
||||
|
||||
const size_t n_inst = instances.size();
|
||||
out.instance_to_chunk.assign(n_inst, 0);
|
||||
|
||||
// Per-instance centroid + AABB → we'll split by centroid, but the
|
||||
// unique-mesh-bytes test uses the actual mesh table.
|
||||
auto inst_center = [&](uint32_t i) {
|
||||
return std::array<float, 3>{
|
||||
0.5f * (instances[i].world_aabb_min[0] + instances[i].world_aabb_max[0]),
|
||||
0.5f * (instances[i].world_aabb_min[1] + instances[i].world_aabb_max[1]),
|
||||
0.5f * (instances[i].world_aabb_min[2] + instances[i].world_aabb_max[2])};
|
||||
};
|
||||
|
||||
// Initial work item: all instances + their union AABB.
|
||||
struct WorkItem {
|
||||
std::vector<uint32_t> instance_ids;
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
};
|
||||
WorkItem root;
|
||||
root.instance_ids.reserve(n_inst);
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
root.aabb_min[a] = std::numeric_limits<float>::infinity();
|
||||
root.aabb_max[a] = -std::numeric_limits<float>::infinity();
|
||||
}
|
||||
for (uint32_t i = 0; i < uint32_t(n_inst); ++i) {
|
||||
root.instance_ids.push_back(i);
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
root.aabb_min[a] = std::min(root.aabb_min[a], instances[i].world_aabb_min[a]);
|
||||
root.aabb_max[a] = std::max(root.aabb_max[a], instances[i].world_aabb_max[a]);
|
||||
}
|
||||
}
|
||||
|
||||
// Computes the total vertex bytes of the unique meshes referenced by
|
||||
// a bucket. Used as the primary stop condition.
|
||||
auto bucket_vertex_bytes = [&](const std::vector<uint32_t>& inst_ids) -> uint64_t {
|
||||
std::set<uint32_t> mesh_ids;
|
||||
for (uint32_t i : inst_ids) {
|
||||
if (instances[i].mesh_id < meshes.size()) {
|
||||
mesh_ids.insert(instances[i].mesh_id);
|
||||
}
|
||||
}
|
||||
uint64_t total = 0;
|
||||
for (uint32_t mi : mesh_ids) {
|
||||
total += uint64_t(meshes[mi].vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
// Emit a leaf bucket from a work item.
|
||||
auto emit_leaf = [&](WorkItem&& w) {
|
||||
const uint32_t ci = uint32_t(out.chunk_mesh_ids.size());
|
||||
std::set<uint32_t> unique_meshes;
|
||||
for (uint32_t i : w.instance_ids) {
|
||||
out.instance_to_chunk[i] = ci;
|
||||
if (instances[i].mesh_id < meshes.size()) {
|
||||
unique_meshes.insert(instances[i].mesh_id);
|
||||
}
|
||||
}
|
||||
out.chunk_mesh_ids.emplace_back(unique_meshes.begin(), unique_meshes.end());
|
||||
};
|
||||
|
||||
// Octree subdivision via a work stack. A bucket is final when it fits
|
||||
// the byte budget AND the instance cap. Degenerate cases (1 instance
|
||||
// but still too big — e.g. a single huge mesh) also terminate to
|
||||
// avoid infinite recursion.
|
||||
std::vector<WorkItem> stack;
|
||||
stack.push_back(std::move(root));
|
||||
while (!stack.empty()) {
|
||||
WorkItem w = std::move(stack.back());
|
||||
stack.pop_back();
|
||||
|
||||
const uint64_t vbytes = bucket_vertex_bytes(w.instance_ids);
|
||||
const bool fits_bytes = vbytes <= WGPU_CHUNK_VERTEX_BYTES_LIMIT;
|
||||
const bool fits_count = w.instance_ids.size() <= spatial_max_instances_;
|
||||
if ((fits_bytes && fits_count) || w.instance_ids.size() <= 1) {
|
||||
emit_leaf(std::move(w));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split into 8 octants around centre. Empty octants skipped.
|
||||
const float cx = 0.5f * (w.aabb_min[0] + w.aabb_max[0]);
|
||||
const float cy = 0.5f * (w.aabb_min[1] + w.aabb_max[1]);
|
||||
const float cz = 0.5f * (w.aabb_min[2] + w.aabb_max[2]);
|
||||
WorkItem octs[8];
|
||||
for (auto& o : octs) {
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
o.aabb_min[a] = std::numeric_limits<float>::infinity();
|
||||
o.aabb_max[a] = -std::numeric_limits<float>::infinity();
|
||||
}
|
||||
}
|
||||
for (uint32_t i : w.instance_ids) {
|
||||
const auto c = inst_center(i);
|
||||
const int idx = (c[0] > cx ? 1 : 0)
|
||||
| (c[1] > cy ? 2 : 0)
|
||||
| (c[2] > cz ? 4 : 0);
|
||||
octs[idx].instance_ids.push_back(i);
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
octs[idx].aabb_min[a] = std::min(octs[idx].aabb_min[a], instances[i].world_aabb_min[a]);
|
||||
octs[idx].aabb_max[a] = std::max(octs[idx].aabb_max[a], instances[i].world_aabb_max[a]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pathological case: every instance falls into the SAME octant —
|
||||
// centroids cluster but AABBs still span. Emit the leaf as-is
|
||||
// rather than infinite-recurse. Common cause: many instances
|
||||
// sharing one mesh that's a long thin slab spanning the bucket.
|
||||
int non_empty = 0;
|
||||
for (const auto& o : octs) if (!o.instance_ids.empty()) ++non_empty;
|
||||
if (non_empty <= 1) {
|
||||
emit_leaf(std::move(w));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& o : octs) {
|
||||
if (!o.instance_ids.empty()) stack.push_back(std::move(o));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::applyNavPreset(const char* name) {
|
||||
// Matches GL AppSettings::NavPreset semantics exactly.
|
||||
// blender — Orbit MMB, Pan Shift+MMB (default)
|
||||
|
||||
@@ -143,19 +143,6 @@ private:
|
||||
float chunkScreenAreaPx(const WgpuModelGpuData::Chunk& c,
|
||||
const QMatrix4x4& vp_mat) const;
|
||||
|
||||
// Octree-style spatial planner. Produces (a) per-bucket mesh_ids — a
|
||||
// mesh may appear in multiple buckets if its instances scattered, and
|
||||
// (b) per-instance bucket assignment. Each bucket carries its own
|
||||
// mesh data in its chunk pool slice (duplicated when shared). Stop
|
||||
// condition: bucket's union vertex bytes ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT
|
||||
// and instance count ≤ spatial_max_instances_. See task #55.
|
||||
struct SpatialPlan {
|
||||
std::vector<std::vector<uint32_t>> chunk_mesh_ids;
|
||||
std::vector<uint32_t> instance_to_chunk;
|
||||
};
|
||||
SpatialPlan planSpatialChunks(const std::vector<InstanceCpu>& instances,
|
||||
const std::vector<MeshInfo>& meshes) const;
|
||||
|
||||
public:
|
||||
|
||||
// Queue a one-shot framebuffer capture: the next rendered frame is
|
||||
@@ -536,20 +523,6 @@ private:
|
||||
// and is per-model the right granularity?).
|
||||
bool cull_threads_enabled_ = true;
|
||||
|
||||
// WGPU_SPATIAL_BUCKETS=1 swaps the streaming chunk planner from the
|
||||
// mesh-keyed Morton+greedy algorithm to an octree-style spatial
|
||||
// subdivision of INSTANCES. Same mesh may appear in multiple buckets
|
||||
// (data duplicated) when its instances scatter — this is the central
|
||||
// trade for tight per-chunk AABBs that actually match what cull and
|
||||
// priority code want. See task #55.
|
||||
bool spatial_buckets_enabled_ = false;
|
||||
// Stop-subdividing thresholds for the octree planner. A cell becomes a
|
||||
// leaf bucket when (a) the union vertex bytes of meshes its instances
|
||||
// reference fits the chunk budget, AND (b) instance count is below the
|
||||
// cap. Tunable via WGPU_SPATIAL_BUCKET_MAX_INSTS env var so we can
|
||||
// sweep without rebuild during the prototype phase.
|
||||
uint32_t spatial_max_instances_ = 5000;
|
||||
|
||||
public:
|
||||
// Master switch for HiZ occlusion. OFF by default — has two issues vs
|
||||
// the GL backend on this codepath (see task #58):
|
||||
|
||||
Reference in New Issue
Block a user