Route bonsai through wgpu; delete the GL backend

Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.

SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
  the StreamingSidecar off to the new applyCachedModel. Field accesses
  inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
  finalizeModel). The applyLodExtension call is dropped — wgpu has no
  live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
  next open.

Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
  SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
  116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
  rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
  ../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
  dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
  duration of the migration; the GL-side IfcViewer also publicly links
  IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.

GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
  Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
  viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
  removed. The first has no replacement (wgpu doesn't use a per-instance
  BVH); the latter two are ported separately. test_lod_builder,
  test_sidecar_cache, test_instanced_geometry, test_federation remain
  (backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
  of the surviving translation units reach for them.

Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
  SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
  IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-01 17:38:33 +10:00
parent 9c067d1d0e
commit 2981500b3b
40 changed files with 208 additions and 8008 deletions
+31 -28
View File
@@ -37,7 +37,7 @@ void SceneLoader::setShouldWriteSidecar(bool enabled) {
should_write_sidecar_ = enabled;
}
SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent)
SceneLoader::SceneLoader(WgpuViewportWindow* viewport, QObject* parent)
: QObject(parent), viewport_(viewport)
{
connect(&element_poll_timer_, &QTimer::timeout,
@@ -189,11 +189,11 @@ void SceneLoader::startNextLoad() {
joinSidecarThread();
sidecar_read_thread_ = std::thread([this, ifc_path, mid, is_sidecar_source]() {
QElapsedTimer rt; rt.start();
auto cached = readSidecar(ifc_path);
qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached));
auto cached = readSidecarMetadataOnly(ifc_path);
qDebug(" Sidecar metadata read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
auto result = std::make_shared<std::optional<StreamingSidecar>>(std::move(cached));
QMetaObject::invokeMethod(this, [this, mid, result, is_sidecar_source]() {
if (*result && !(*result)->instances.empty()) {
if (*result && !(*result)->meta.instances.empty()) {
applySidecarData(mid, std::move(**result));
if (!is_sidecar_source) {
startDataSourceLoad(mid);
@@ -233,36 +233,37 @@ void SceneLoader::startStreamLoadFor(uint32_t mid) {
m.file_path.toStdString(), next_object_id_, loading_model_id_);
}
void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
void SceneLoader::applySidecarData(uint32_t mid, StreamingSidecar metadata) {
auto it = models_.find(mid);
if (it == models_.end()) return;
auto& model = it->second;
SidecarData& d = metadata.meta;
qDebug("Sidecar hit: %s (%zu verts, %zu indices, %zu meshes, %zu instances, %zu elements)",
qDebug("Sidecar hit: %s (%zu metadata bytes, %zu indices, %zu meshes, %zu instances, %zu elements)",
model.file_path.toStdString().c_str(),
data.vertices.size() / INSTANCED_VERTEX_STRIDE_BYTES,
data.indices.size(),
data.meshes.size(),
data.instances.size(),
data.elements.size());
size_t(metadata.vertex_total_bytes),
size_t(metadata.index_total_count),
d.meshes.size(),
d.instances.size(),
d.elements.size());
// Rebase object/model IDs onto the current session's ID space. Two
// cached models both starting at object_id=1 would collide otherwise.
uint32_t min_oid = UINT32_MAX;
for (const auto& pe : data.elements) {
for (const auto& pe : d.elements) {
if (pe.object_id < min_oid) min_oid = pe.object_id;
}
uint32_t oid_offset = 0;
if (!data.elements.empty() && min_oid < UINT32_MAX) {
if (!d.elements.empty() && min_oid < UINT32_MAX) {
oid_offset = next_object_id_ - min_oid;
}
for (auto& pe : data.elements) {
for (auto& pe : d.elements) {
pe.object_id += oid_offset;
pe.model_id = mid;
if (pe.object_id >= next_object_id_)
next_object_id_ = pe.object_id + 1;
}
for (auto& inst : data.instances) {
for (auto& inst : d.instances) {
inst.object_id += oid_offset;
inst.model_id = mid;
}
@@ -273,26 +274,26 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
// .ifc/.rdb sibling is absent.
{
ModelGeoref& gr = model.georef;
gr.has_coordinate_operation = data.has_coordinate_operation != 0;
gr.has_coordinate_operation = d.has_coordinate_operation != 0;
Eigen::Map<const Eigen::Matrix<double, 4, 4, Eigen::ColMajor>> M(
data.coordinate_operation_meters);
d.coordinate_operation_meters);
gr.coordinate_operation_meters = M;
gr.units.project_length_to_meters = data.project_length_to_meters;
gr.units.map_unit_to_meters = data.map_unit_to_meters;
gr.units.project_length_to_meters = d.project_length_to_meters;
gr.units.map_unit_to_meters = d.map_unit_to_meters;
model.has_georef = true;
}
if (!data.instances.empty() && !model.has_first_placement) {
if (!d.instances.empty() && !model.has_first_placement) {
using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
model.first_placement =
Eigen::Map<const Mat4dCol>(data.instances[0].placement_transformation);
Eigen::Map<const Mat4dCol>(d.instances[0].placement_transformation);
model.has_first_placement = true;
}
std::vector<PackedElementInfo> elements = std::move(data.elements);
std::string stbl = std::move(data.string_table);
std::vector<PackedElementInfo> elements = std::move(d.elements);
std::string stbl = std::move(d.string_table);
viewport_->applyCachedModel(mid, std::move(data));
viewport_->applyCachedModel(mid, std::move(metadata));
emit sidecarElementsReady(mid, std::move(elements), std::move(stbl));
@@ -397,8 +398,11 @@ void SceneLoader::onStreamerFinished() {
next_object_id_ = m.streamer->lastObjectId();
viewport_->finalizeModel(mid);
// Sidecar finalize + LOD application + disk write. Runs on the
// GUI thread so applyLodExtension's GL touches are safe.
// Sidecar finalize + disk write. Wgpu has no live LOD1 apply —
// LOD1 indices land in the on-disk sidecar and are picked up
// on the *next* open of this file; first-session view is
// LOD0-only. Acceptable trade-off vs reallocating chunk index
// slices live to splice LOD1 in.
if (m.sidecar_builder) {
ModelGeoref georef;
if (auto* file = m.streamer->ifcFile()) {
@@ -406,7 +410,6 @@ void SceneLoader::onStreamerFinished() {
}
QElapsedTimer wt; wt.start();
SidecarData data = m.sidecar_builder->finalize(georef, m.streamed_elements);
viewport_->applyLodExtension(mid, data);
const bool ok = writeSidecar(m.file_path.toStdString(), data);
qDebug(" Sidecar finalize + write: %lld ms (%s)",
wt.elapsed(), ok ? "ok" : "FAILED");