diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 9574ec0b18..e24902dadf 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -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> chunk_mesh_ids; std::vector 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 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& instances, - const std::vector& 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{ - 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 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::infinity(); - root.aabb_max[a] = -std::numeric_limits::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& inst_ids) -> uint64_t { - std::set 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 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 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::infinity(); - o.aabb_max[a] = -std::numeric_limits::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) diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index d0ad963e3f..fc20f4b5a7 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -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> chunk_mesh_ids; - std::vector instance_to_chunk; - }; - SpatialPlan planSpatialChunks(const std::vector& instances, - const std::vector& 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):