From 8da099345723251a2ca84ae165688cd5e4f35250 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 5 Jun 2026 14:04:57 +1000 Subject: [PATCH] ifcviewer: move scene mutators + releaseWgpuModelGpuData into ViewportCore (#84-f) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the eight scene-mutation methods that drive bonsai's load/unload and georeference setters, plus the per-model GPU teardown helper. All are mechanical transplants — no logic change — so behaviour stays identical; only the owner has changed. Methods moved (ViewportWindow public-API methods stay as forwarders to keep the bonsai-side callers compiling): removeModel / resetScene / hideModel / showModel setFederatedFalseOrigin setModelCoordinateOperation setModelTransformation recomposeAndUploadModel State moved: bool wgpu_initialized_ (storage → core_, alias kept in VW for the initWgpu call site that still flips it; goes when initWgpu moves) Free function moved: releaseWgpuModelGpuData(ModelGpuData&, BufferPool&) → ViewportCore.cpp (must live in IfcViewerCore now that ViewportCore.cpp's removeModel / resetScene call it; ViewportWindow.cpp's remaining two call sites continue to resolve through ModelGpuData.h's declaration — same linker view, different definition TU) The `if (isExposed()) requestUpdate()` Qt pattern inside the moved bodies became `host_->requestFrame()` since ViewportCore can't see QWindow; the desktop ViewportHost override at the bottom of ViewportWindow.cpp continues to translate that into requestUpdate(). Builds: desktop / bonsai / web all green. Tests 100/100. --- src/ifcviewer/ViewportCore.cpp | 143 +++++++++++++++++++++++++++++ src/ifcviewer/ViewportCore.h | 35 +++++++ src/ifcviewer/ViewportWindow.cpp | 153 +++++-------------------------- src/ifcviewer/ViewportWindow.h | 2 +- 4 files changed, 203 insertions(+), 130 deletions(-) diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 8026507ecf..12554c63a6 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -19,11 +19,154 @@ #include "ViewportCore.h" +#include +#include +#include + #include "InstanceCompose.h" ViewportCore::ViewportCore(ViewportHost* host) : host_(host) {} ViewportCore::~ViewportCore() = default; +// Tear down a model's per-chunk GPU resources, free its pool slices, +// and reset all the bookkeeping vectors so the slot can be reused. +// Static because callers from outside this TU still live in +// ViewportWindow.cpp; ModelGpuData.h's declaration keeps the +// inter-TU contract. +void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool) { + for (auto& c : m.chunks) { + if (c.bind_group) { wgpuBindGroupRelease(c.bind_group); c.bind_group = nullptr; } + if (c.vertex_slice.valid()) { + pool.free(c.vertex_slice); + c.vertex_slice = {}; + } + if (c.index_slice.valid()) { + pool.free(c.index_slice); + c.index_slice = {}; + } + if (c.visible_draws_buffer) { wgpuBufferRelease(c.visible_draws_buffer); c.visible_draws_buffer = nullptr; } + if (c.prefix_sums_buffer) { wgpuBufferRelease(c.prefix_sums_buffer); c.prefix_sums_buffer = nullptr; } + if (c.per_chunk_uniform) { wgpuBufferRelease(c.per_chunk_uniform); c.per_chunk_uniform = nullptr; } + } + m.chunks.clear(); + m.mesh_chunk_idx.clear(); + m.mesh_chunk_local_base_vertex.clear(); + m.mesh_chunk_local_ebo_first_u32.clear(); + m.mesh_chunk_local_lod1_first_u32.clear(); + m.instance_chunk_idx.clear(); + m.instance_base_vertex.clear(); + m.instance_ebo_first_u32.clear(); + m.instance_lod1_first_u32.clear(); + if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; } + if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; } + m.vertex_bytes = 0; + m.index_count = 0; + m.mesh_count = 0; + m.instance_count = 0; + m.meshes.clear(); + m.instances.clear(); +} + +// ---- Scene mutators ------------------------------------------------------- + +void ViewportCore::removeModel(uint32_t model_id) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + releaseWgpuModelGpuData(it->second, pool_); + models_gpu_.erase(it); + host_->requestFrame(); +} + +void ViewportCore::resetScene() { + for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_); + models_gpu_.clear(); + host_->requestFrame(); +} + +void ViewportCore::hideModel(uint32_t model_id) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end() || it->second.hidden) return; + it->second.hidden = true; + host_->requestFrame(); +} + +void ViewportCore::showModel(uint32_t model_id) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end() || !it->second.hidden) return; + it->second.hidden = false; + host_->requestFrame(); +} + +void ViewportCore::setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters) { + if (federated_false_origin_meters_ == matrix_meters) return; + federated_false_origin_meters_ = matrix_meters; + for (auto& kv : models_gpu_) recomposeAndUploadModel(kv.first); +} + +void ViewportCore::setModelCoordinateOperation(uint32_t model_id, + const Eigen::Matrix4d& matrix_meters) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + if (it->second.coordinate_operation_meters == matrix_meters) return; + it->second.coordinate_operation_meters = matrix_meters; + recomposeAndUploadModel(model_id); +} + +void ViewportCore::setModelTransformation(uint32_t model_id, + const Eigen::Matrix4d& matrix_meters) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + if (it->second.model_transformation_meters == matrix_meters) return; + it->second.model_transformation_meters = matrix_meters; + recomposeAndUploadModel(model_id); +} + +void ViewportCore::recomposeAndUploadModel(uint32_t model_id) { + if (!wgpu_initialized_) return; + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + ModelGpuData& m = it->second; + if (m.instances.empty() || m.instance_storage == nullptr) return; + + std::vector gpu(m.instances.size()); + for (size_t i = 0; i < m.instances.size(); ++i) { + InstanceCpu& inst = m.instances[i]; + composeInstanceFromPlacement(inst, m); + + InstanceGpu& dst = gpu[i]; + std::memcpy(dst.transform, inst.transform, sizeof(dst.transform)); + dst.object_id = inst.object_id; + dst.color_override_rgba8 = inst.color_override_rgba8; + dst.mesh_id = inst.mesh_id; + dst._pad1 = 0; + } + wgpuQueueWriteBuffer(queue_, m.instance_storage, 0, + gpu.data(), gpu.size() * sizeof(InstanceGpu)); + + // Per-chunk world AABBs are derived from instance world AABBs; they + // drive chunk-level frustum cull and the streaming priority, so they + // must follow the recompose. Reset to ±inf and re-fold every chunk's + // instances. Streaming chunks that haven't yet been assigned + // instance_ids (extremely rare path) just stay at ±inf and naturally + // fall out of frustum tests until the next load completes. + for (auto& c : m.chunks) { + c.aabb_min[0] = c.aabb_min[1] = c.aabb_min[2] = + std::numeric_limits::infinity(); + c.aabb_max[0] = c.aabb_max[1] = c.aabb_max[2] = + -std::numeric_limits::infinity(); + for (uint32_t inst_idx : c.instance_ids) { + if (inst_idx >= m.instances.size()) continue; + const InstanceCpu& inst = m.instances[inst_idx]; + for (int a = 0; a < 3; ++a) { + c.aabb_min[a] = std::min(c.aabb_min[a], inst.world_aabb_min[a]); + c.aabb_max[a] = std::max(c.aabb_max[a], inst.world_aabb_max[a]); + } + } + } + + host_->requestFrame(); +} + bool ViewportCore::findInstance(uint32_t object_id, InstanceCompose::InstanceLookup& out) const { return InstanceCompose::findInstanceInModels(object_id, models_gpu_, out); diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 604a4c31c8..b99ec6e0a8 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -81,6 +81,34 @@ public: bool firstGeometryPointWorldM(uint32_t model_id, Eigen::Vector3d& out) const; + // ---- Scene mutators ----------------------------------------------------- + // + // All of these flip scene state (or post a recompose) and ask the + // host to schedule another frame via host_->requestFrame(). The host + // is responsible for coalescing those requests (Qt's requestUpdate + // does it natively; the web host wraps requestAnimationFrame). + + void removeModel(uint32_t model_id); + void resetScene(); + void hideModel(uint32_t model_id); + void showModel(uint32_t model_id); + + // Federation matrix setters. Each writes to model state and posts + // a recompose so per-instance world matrices stay consistent with + // the configured georef + transformation pipeline. + void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters); + void setModelCoordinateOperation(uint32_t model_id, + const Eigen::Matrix4d& matrix_meters); + void setModelTransformation(uint32_t model_id, + const Eigen::Matrix4d& matrix_meters); + + // Walk every instance of `model_id`, recompose its transform from + // the current federation matrices, refresh per-chunk world AABBs, + // and re-upload InstanceGpu[] into m.instance_storage. No-op if + // the model is unknown, has no instances, or wgpu init hasn't + // completed. + void recomposeAndUploadModel(uint32_t model_id); + // Friend access for ViewportWindow's reference proxies. As each // render method moves into ViewportCore it stops needing these // (it touches the fields directly); once everything has migrated @@ -162,6 +190,13 @@ private: // every instance composition so geometry rebased through a large // model offset doesn't lose float32 precision near the GPU origin. Eigen::Matrix4d federated_false_origin_meters_ = Eigen::Matrix4d::Identity(); + + // Flips true once initWgpu has finished bringing up device + queue + // (still done on the ViewportWindow side today — moves with #84-i). + // Any method that uploads or encodes work checks this guard so a + // queued setter that runs before init becomes a no-op rather than + // crashing on a null device. + bool wgpu_initialized_ = false; }; #endif // VIEWPORTCORE_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index a92e29ec72..6f941da2b3 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -222,39 +222,7 @@ static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue, return buf; } -void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool) { - for (auto& c : m.chunks) { - if (c.bind_group) { wgpuBindGroupRelease(c.bind_group); c.bind_group = nullptr; } - if (c.vertex_slice.valid()) { - pool.free(c.vertex_slice); - c.vertex_slice = {}; - } - if (c.index_slice.valid()) { - pool.free(c.index_slice); - c.index_slice = {}; - } - if (c.visible_draws_buffer) { wgpuBufferRelease(c.visible_draws_buffer); c.visible_draws_buffer = nullptr; } - if (c.prefix_sums_buffer) { wgpuBufferRelease(c.prefix_sums_buffer); c.prefix_sums_buffer = nullptr; } - if (c.per_chunk_uniform) { wgpuBufferRelease(c.per_chunk_uniform); c.per_chunk_uniform = nullptr; } - } - m.chunks.clear(); - m.mesh_chunk_idx.clear(); - m.mesh_chunk_local_base_vertex.clear(); - m.mesh_chunk_local_ebo_first_u32.clear(); - m.mesh_chunk_local_lod1_first_u32.clear(); - m.instance_chunk_idx.clear(); - m.instance_base_vertex.clear(); - m.instance_ebo_first_u32.clear(); - m.instance_lod1_first_u32.clear(); - if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; } - if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; } - m.vertex_bytes = 0; - m.index_count = 0; - m.mesh_count = 0; - m.instance_count = 0; - m.meshes.clear(); - m.instances.clear(); -} +// releaseWgpuModelGpuData moved to ViewportCore.cpp (IfcViewerCore now needs it). // ----------------------------------------------------------------------------- // WGSL main pipeline — cross-mesh vertex pulling. @@ -639,7 +607,8 @@ ViewportWindow::ViewportWindow(QWindow* parent) models_gpu_ (core_.models_gpu_), next_model_id_ (core_.next_model_id_), next_object_id_ (core_.next_object_id_), - federated_false_origin_meters_(core_.federated_false_origin_meters_) { + federated_false_origin_meters_(core_.federated_false_origin_meters_), + wgpu_initialized_(core_.wgpu_initialized_) { // wgpu doesn't need a GL context; we just need a real native window // whose backing layer matches the GPU API wgpu will drive. // @@ -1326,104 +1295,30 @@ void ViewportWindow::finalizeModel(uint32_t model_id) { << " idx=" << raw_indices.size(); } -void ViewportWindow::removeModel(uint32_t model_id) { - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end()) return; - releaseWgpuModelGpuData(it->second, pool_); - models_gpu_.erase(it); - if (isExposed()) requestUpdate(); +// removeModel / resetScene / hideModel / showModel / +// setFederatedFalseOrigin / setModelCoordinateOperation / +// setModelTransformation / recomposeAndUploadModel moved into +// ViewportCore (#84-f). The public-API entry points below forward +// so existing bonsai-side callers don't have to change. + +void ViewportWindow::removeModel(uint32_t model_id) { core_.removeModel(model_id); } +void ViewportWindow::resetScene() { core_.resetScene(); } +void ViewportWindow::hideModel(uint32_t model_id) { core_.hideModel(model_id); } +void ViewportWindow::showModel(uint32_t model_id) { core_.showModel(model_id); } + +void ViewportWindow::setFederatedFalseOrigin(const Eigen::Matrix4d& m) { + core_.setFederatedFalseOrigin(m); } - -void ViewportWindow::resetScene() { - for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_); - models_gpu_.clear(); - if (isExposed()) requestUpdate(); +void ViewportWindow::setModelCoordinateOperation(uint32_t mid, + const Eigen::Matrix4d& m) { + core_.setModelCoordinateOperation(mid, m); } - -void ViewportWindow::hideModel(uint32_t model_id) { - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end() || it->second.hidden) return; - it->second.hidden = true; - if (isExposed()) requestUpdate(); +void ViewportWindow::setModelTransformation(uint32_t mid, + const Eigen::Matrix4d& m) { + core_.setModelTransformation(mid, m); } - -void ViewportWindow::showModel(uint32_t model_id) { - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end() || !it->second.hidden) return; - it->second.hidden = false; - if (isExposed()) requestUpdate(); -} - -void ViewportWindow::setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters) { - if (federated_false_origin_meters_ == matrix_meters) return; - federated_false_origin_meters_ = matrix_meters; - for (auto& kv : models_gpu_) recomposeAndUploadModel(kv.first); -} - -void ViewportWindow::setModelCoordinateOperation(uint32_t model_id, - const Eigen::Matrix4d& matrix_meters) { - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end()) return; - if (it->second.coordinate_operation_meters == matrix_meters) return; - it->second.coordinate_operation_meters = matrix_meters; - recomposeAndUploadModel(model_id); -} - -void ViewportWindow::setModelTransformation(uint32_t model_id, - const Eigen::Matrix4d& matrix_meters) { - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end()) return; - if (it->second.model_transformation_meters == matrix_meters) return; - it->second.model_transformation_meters = matrix_meters; - recomposeAndUploadModel(model_id); -} - -// composeInstanceFromPlacement moved to ViewportCore (#84-d). - -void ViewportWindow::recomposeAndUploadModel(uint32_t model_id) { - if (!wgpu_initialized_) return; - auto it = models_gpu_.find(model_id); - if (it == models_gpu_.end()) return; - ModelGpuData& m = it->second; - if (m.instances.empty() || m.instance_storage == nullptr) return; - - std::vector gpu(m.instances.size()); - for (size_t i = 0; i < m.instances.size(); ++i) { - InstanceCpu& inst = m.instances[i]; - core_.composeInstanceFromPlacement(inst, m); - - InstanceGpu& dst = gpu[i]; - std::memcpy(dst.transform, inst.transform, sizeof(dst.transform)); - dst.object_id = inst.object_id; - dst.color_override_rgba8 = inst.color_override_rgba8; - dst.mesh_id = inst.mesh_id; - dst._pad1 = 0; - } - wgpuQueueWriteBuffer(queue_, m.instance_storage, 0, - gpu.data(), gpu.size() * sizeof(InstanceGpu)); - - // Per-chunk world AABBs are derived from instance world AABBs; they - // drive chunk-level frustum cull and the streaming priority, so they - // must follow the recompose. Reset to ±inf and re-fold every chunk's - // instances. Streaming chunks that haven't yet been assigned - // instance_ids (extremely rare path) just stay at ±inf and naturally - // fall out of frustum tests until the next load completes. - for (auto& c : m.chunks) { - c.aabb_min[0] = c.aabb_min[1] = c.aabb_min[2] = - std::numeric_limits::infinity(); - c.aabb_max[0] = c.aabb_max[1] = c.aabb_max[2] = - -std::numeric_limits::infinity(); - for (uint32_t inst_idx : c.instance_ids) { - if (inst_idx >= m.instances.size()) continue; - const InstanceCpu& inst = m.instances[inst_idx]; - for (int a = 0; a < 3; ++a) { - c.aabb_min[a] = std::min(c.aabb_min[a], inst.world_aabb_min[a]); - c.aabb_max[a] = std::max(c.aabb_max[a], inst.world_aabb_max[a]); - } - } - } - - if (isExposed()) requestUpdate(); +void ViewportWindow::recomposeAndUploadModel(uint32_t mid) { + core_.recomposeAndUploadModel(mid); } bool ViewportWindow::findInstance(uint32_t object_id, InstanceLookup& out) const { diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 237555db61..916b19d190 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -634,7 +634,7 @@ private: // is unknown, has no instances, or wgpu init hasn't completed. void recomposeAndUploadModel(uint32_t model_id); - bool wgpu_initialized_ = false; + bool& wgpu_initialized_; int configured_w_ = 0; int configured_h_ = 0;