From 8b8fafa6984c3d0efb27125f879b53ff164d66bd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 16 May 2026 21:11:27 +1000 Subject: [PATCH] Unify sidecar production via SidecarBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed HeadlessSidecarBuilder to SidecarBuilder and reused it for live loads. SceneLoader now constructs one per stream load, forwards meshReady /instanceReady chunks alongside the viewport upload, and finalizes + writes the sidecar at onStreamerFinished — no more GPU readback path via ViewportWindow::snapshotModel (removed). Same code path now produces sidecars for both live loads and the .rdbview offline export. Sidecar use is opt-in per direction via SceneLoader::setShouldReadSidecar and setShouldWriteSidecar; both default off so embedders that don't want caching get a pure-streaming loader. ifcviewer-full and ifcviewer-minimal opt in. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-full/MainWindow.cpp | 2 - src/ifcviewer-full/SessionState.cpp | 3 +- src/ifcviewer-full/SessionState.h | 4 - .../modules/models/Commands.cpp | 71 +---------- src/ifcviewer-full/modules/models/Commands.h | 11 -- src/ifcviewer-minimal/MinimalWindow.cpp | 2 + src/ifcviewer/SceneLoader.cpp | 93 +++++++++++--- src/ifcviewer/SceneLoader.h | 31 ++++- ...sSidecarBuilder.cpp => SidecarBuilder.cpp} | 115 +++++++++--------- ...dlessSidecarBuilder.h => SidecarBuilder.h} | 57 ++++++--- src/ifcviewer/VertexQuantization.h | 5 +- src/ifcviewer/ViewportWindow.cpp | 22 ---- src/ifcviewer/ViewportWindow.h | 6 - 13 files changed, 212 insertions(+), 210 deletions(-) rename src/ifcviewer/{HeadlessSidecarBuilder.cpp => SidecarBuilder.cpp} (83%) rename src/ifcviewer/{HeadlessSidecarBuilder.h => SidecarBuilder.h} (52%) diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 4a789fecc6..867c8898e9 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -470,8 +470,6 @@ void MainWindow::setupLoader() { session_state_->createLoader(viewport_widget_->viewport()); viewport_view_ = new modules::viewport::ViewportView( session_state_, viewport_widget_->viewport(), this); - modules::models::commands::addHandlers( - *session_state_, *viewport_widget_->viewport(), *this); // Load errors surface through SessionState as a session-level signal; the // status text + progress are already cleared there, we only show the modal. diff --git a/src/ifcviewer-full/SessionState.cpp b/src/ifcviewer-full/SessionState.cpp index aa569b9801..cd2d4b9194 100644 --- a/src/ifcviewer-full/SessionState.cpp +++ b/src/ifcviewer-full/SessionState.cpp @@ -36,6 +36,8 @@ SessionState::SessionState(QObject* parent) void SessionState::createLoader(ViewportWindow* viewport) { Q_ASSERT(!loader_); loader_ = new SceneLoader(viewport, this); + loader_->setShouldReadSidecar(true); + loader_->setShouldWriteSidecar(true); element_registry_->bindLoader(loader_); auto format_elapsed = [](qint64 ms) { @@ -69,7 +71,6 @@ void SessionState::createLoader(ViewportWindow* viewport) { .arg(format_elapsed(elapsed_ms))); endProgress(); emit modelGeometryReady(mid); - emit modelGeometryStreamed(mid); }); connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t mid) { setStatusMessage("Cancelled", loader_->displayName(mid)); diff --git a/src/ifcviewer-full/SessionState.h b/src/ifcviewer-full/SessionState.h index 5602a25ad4..7e9f20dfbf 100644 --- a/src/ifcviewer-full/SessionState.h +++ b/src/ifcviewer-full/SessionState.h @@ -90,10 +90,6 @@ signals: // for both sidecar-cache and stream loads; subscribers that just need to // re-derive view state (e.g. ViewportView::refresh) listen to this. void modelGeometryReady(uint32_t model_id); - // Fires only after a stream load — i.e. when no sidecar cache existed - // yet. The sidecar-write subscriber listens to this so it doesn't - // re-persist a cache that was just read from disk. - void modelGeometryStreamed(uint32_t model_id); // Fires when SceneLoader reports a load failure. SessionState turns the // raw loader signal into a session-level one so views (e.g. the MessageBox) // can subscribe without touching the loader directly. diff --git a/src/ifcviewer-full/modules/models/Commands.cpp b/src/ifcviewer-full/modules/models/Commands.cpp index b44daf4504..f9fe818f5e 100644 --- a/src/ifcviewer-full/modules/models/Commands.cpp +++ b/src/ifcviewer-full/modules/models/Commands.cpp @@ -26,10 +26,8 @@ #include "../../ElementRegistry.h" #include "../../SessionState.h" #include "../../../ifcviewer/Federation.h" -#include "../../../ifcviewer/HeadlessSidecarBuilder.h" -#include "../../../ifcviewer/LodBuilder.h" #include "../../../ifcviewer/SceneLoader.h" -#include "../../../ifcviewer/SidecarCache.h" +#include "../../../ifcviewer/SidecarBuilder.h" #include "../../../ifcviewer/ViewportWindow.h" #include "../../../ifcgeom/Serializer.h" #include "../../../serializers/document_serializer_plugin.h" @@ -173,11 +171,6 @@ void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QStri s.setStatusMessage("Models", "Model removed"); } -void addHandlers(SessionState& s, ViewportWindow& vp, QObject& context) { - QObject::connect(&s, &SessionState::modelGeometryStreamed, &context, - [&s, &vp](uint32_t mid) { writeSidecarForLoadedModel(s, vp, mid); }); -} - namespace detail { void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids) { @@ -432,7 +425,7 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) { serializer->finalize(); serializer.reset(); - HeadlessSidecarBuilder builder; + SidecarBuilder builder; if (!builder.build(input_path, tmp_anchor)) { throw ifcopenshell::exception( ("Sidecar build failed: " + builder.lastError()).toStdString()); @@ -531,64 +524,4 @@ void openSettings(SessionState& s, QWidget& host) { dialog.exec(); } -void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid) { - SceneLoader* loader = s.loader(); - if (!loader) return; - - SidecarData sidecar_data; - if (!vp.snapshotModel(mid, sidecar_data)) return; - - if (const ModelGeoref* georef = loader->modelGeoref(mid)) { - sidecar_data.has_coordinate_operation = georef->has_coordinate_operation ? 1 : 0; - Eigen::Map>( - sidecar_data.coordinate_operation_meters) = georef->coordinate_operation_meters; - sidecar_data.project_length_to_meters = georef->units.project_length_to_meters; - sidecar_data.map_unit_to_meters = georef->units.map_unit_to_meters; - } - - if (auto* element_registry = s.elementRegistry()) { - for (const auto& info : element_registry->basicElementInfoForModel(mid)) { - PackedElementInfo packed; - packed.object_id = info.object_id; - packed.model_id = info.model_id; - packed.ifc_id = info.ifc_id; - packed.parent_id = info.parent_id; - - const std::string guid = info.guid.toStdString(); - packed.guid_offset = static_cast(sidecar_data.string_table.size()); - packed.guid_length = static_cast(guid.size()); - sidecar_data.string_table += guid; - - const std::string name = info.name.toStdString(); - packed.name_offset = static_cast(sidecar_data.string_table.size()); - packed.name_length = static_cast(name.size()); - sidecar_data.string_table += name; - - const std::string type = info.type.toStdString(); - packed.type_offset = static_cast(sidecar_data.string_table.size()); - packed.type_length = static_cast(type.size()); - sidecar_data.string_table += type; - - sidecar_data.elements.push_back(packed); - } - } - - QElapsedTimer lod_timer; - lod_timer.start(); - buildLods(sidecar_data); - const LodStats lod_stats = summariseLods(sidecar_data); - qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 " - "(%u tris -> %u tris for those meshes)", - lod_timer.elapsed(), - lod_stats.meshes_with_lod1, lod_stats.meshes_total, - lod_stats.tris_lod0_for_lod1, lod_stats.tris_lod1); - - vp.applyLodExtension(mid, sidecar_data); - - QElapsedTimer sidecar_timer; - sidecar_timer.start(); - const bool ok = writeSidecar(loader->filePath(mid).toStdString(), sidecar_data); - qDebug(" Sidecar write: %lld ms (%s)", sidecar_timer.elapsed(), ok ? "ok" : "FAILED"); -} - } // namespace ifcviewerfull::modules::models::commands diff --git a/src/ifcviewer-full/modules/models/Commands.h b/src/ifcviewer-full/modules/models/Commands.h index 4638b60cc7..7d38dac03f 100644 --- a/src/ifcviewer-full/modules/models/Commands.h +++ b/src/ifcviewer-full/modules/models/Commands.h @@ -47,17 +47,6 @@ void convertIfcToDatabase(SessionState& s, QWidget& host); void exportGeometryDatabase(SessionState& s, QWidget& host); void openSettings(SessionState& s, QWidget& host); -// Snapshots the in-memory geometry + element registry for a freshly streamed -// model and persists it as a sidecar next to the source IFC. Called after -// SceneLoader::loadedFromStream so subsequent loads can skip the stream phase. -void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid); - -// Persistent post-load handler. Call once at app startup. Whenever the -// session reports a model was streamed (not loaded from cache), persists a -// sidecar so the next load skips the stream phase. Connection lifetime is -// tied to `context`. -void addHandlers(SessionState& s, ViewportWindow& vp, QObject& context); - // Internal building blocks shared by commands here and by ProjectController. // These NEVER call notify*() — the caller is responsible for emitting once // at the end of its execution. diff --git a/src/ifcviewer-minimal/MinimalWindow.cpp b/src/ifcviewer-minimal/MinimalWindow.cpp index ebca8f303e..3775637348 100644 --- a/src/ifcviewer-minimal/MinimalWindow.cpp +++ b/src/ifcviewer-minimal/MinimalWindow.cpp @@ -39,6 +39,8 @@ MinimalWindow::MinimalWindow(QWidget* parent) statusBar()->addPermanentWidget(stats_label_); loader_ = new SceneLoader(viewport_, this); + loader_->setShouldReadSidecar(true); + loader_->setShouldWriteSidecar(true); connect(loader_, &SceneLoader::loadStarted, this, &MinimalWindow::onLoadStarted); connect(loader_, &SceneLoader::loadedFromSidecar, diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index 84ada2bf11..2185438400 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -29,6 +29,14 @@ #include #include +void SceneLoader::setShouldReadSidecar(bool enabled) { + should_read_sidecar_ = enabled; +} + +void SceneLoader::setShouldWriteSidecar(bool enabled) { + should_write_sidecar_ = enabled; +} + SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent) : QObject(parent), viewport_(viewport) { @@ -169,6 +177,14 @@ void SceneLoader::startNextLoad() { const bool is_sidecar_source = QFileInfo(model.file_path).suffix().compare("ifcview", Qt::CaseInsensitive) == 0; + // No sidecar read probe when caching is off (and the user didn't pick a + // .ifcview file directly). Skip the background thread and go straight + // to a stream load. + if (!is_sidecar_source && !should_read_sidecar_) { + startStreamLoadFor(mid); + return; + } + // Sidecar read on a background thread so the UI stays responsive. joinSidecarThread(); sidecar_read_thread_ = std::thread([this, ifc_path, mid, is_sidecar_source]() { @@ -195,15 +211,28 @@ void SceneLoader::startNextLoad() { return; } - auto& m = it->second; - connectStreamer(m.streamer); - element_poll_timer_.start(); - m.streamer->loadFile( - m.file_path.toStdString(), next_object_id_, loading_model_id_); + startStreamLoadFor(mid); }, Qt::QueuedConnection); }); } +void SceneLoader::startStreamLoadFor(uint32_t mid) { + auto it = models_.find(mid); + if (it == models_.end()) return; + auto& m = it->second; + // Accumulate sidecar data alongside the GPU upload so the first load + // naturally produces a cache for the next one — no GPU readback at + // finish time. Skipped when caching writes are off. + if (should_write_sidecar_) { + m.sidecar_builder = std::make_unique(); + m.streamed_elements.clear(); + } + connectStreamer(m.streamer); + element_poll_timer_.start(); + m.streamer->loadFile( + m.file_path.toStdString(), next_object_id_, loading_model_id_); +} + void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) { auto it = models_.find(mid); if (it == models_.end()) return; @@ -314,16 +343,27 @@ void SceneLoader::onStreamerProgressChanged(int percent) { void SceneLoader::onStreamerMeshReady(MeshChunk chunk) { viewport_->uploadMeshChunk(chunk); + if (loading_model_id_ != 0) { + auto it = models_.find(loading_model_id_); + if (it != models_.end() && it->second.sidecar_builder) { + it->second.sidecar_builder->onMeshReady(chunk); + } + } } void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) { if (loading_model_id_ != 0) { auto it = models_.find(loading_model_id_); - if (it != models_.end() && !it->second.has_first_placement) { - using Mat4fCol = Eigen::Matrix; - it->second.first_placement = - Eigen::Map(chunk.transform).cast(); - it->second.has_first_placement = true; + if (it != models_.end()) { + if (!it->second.has_first_placement) { + using Mat4fCol = Eigen::Matrix; + it->second.first_placement = + Eigen::Map(chunk.transform).cast(); + it->second.has_first_placement = true; + } + if (it->second.sidecar_builder) { + it->second.sidecar_builder->onInstanceReady(chunk); + } } } viewport_->uploadInstanceChunk(chunk); @@ -335,9 +375,15 @@ void SceneLoader::onElementPollTick() { if (it == models_.end()) return; auto batch = it->second.streamer->drainElements(); - if (!batch.empty()) { - emit streamedElementsReady(loading_model_id_, std::move(batch)); + if (batch.empty()) return; + + // Mirror into the per-model accumulator so finalize() has the full set + // without re-draining (the streamer's queue is consumed by this drain). + if (it->second.sidecar_builder) { + auto& buf = it->second.streamed_elements; + buf.insert(buf.end(), batch.begin(), batch.end()); } + emit streamedElementsReady(loading_model_id_, std::move(batch)); } void SceneLoader::onStreamerFinished() { @@ -348,10 +394,29 @@ void SceneLoader::onStreamerFinished() { if (mid != 0) { auto it = models_.find(mid); if (it != models_.end()) { - next_object_id_ = it->second.streamer->lastObjectId(); + auto& m = it->second; + next_object_id_ = m.streamer->lastObjectId(); viewport_->finalizeModel(mid); - qint64 ms = it->second.load_timer.elapsed(); + // Sidecar finalize + LOD application + disk write. Runs on the + // GUI thread so applyLodExtension's GL touches are safe. + if (m.sidecar_builder) { + ModelGeoref georef; + if (auto* file = m.streamer->ifcFile()) { + georef = computeModelGeoref(file); + } + 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"); + m.sidecar_builder.reset(); + m.streamed_elements.clear(); + m.streamed_elements.shrink_to_fit(); + } + + qint64 ms = m.load_timer.elapsed(); emit loadedFromStream(mid, ms); } } diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index 992b7def3f..495d604628 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "Federation.h" #include "ViewportWindow.h" #include "GeometryStreamer.h" +#include "SidecarBuilder.h" #include "SidecarCache.h" // Drives IFC file loading into a ViewportWindow. Owns the per-model @@ -45,15 +47,27 @@ // // Consumers (MainWindow, MinimalWindow) observe progress through signals // and never touch the streamer, sidecar thread, or queue directly. -// Sidecar *writes* are intentionally left to the consumer: they need the -// consumer's element metadata (guid/name/type strings) which SceneLoader -// does not retain. +// Sidecar *writes* happen automatically as a side effect of stream loads — +// the SidecarBuilder accumulates from the streamer chunks alongside the +// viewport upload, and SceneLoader finalizes + writes the result when the +// stream finishes. No GPU readback involved. class SceneLoader : public QObject { Q_OBJECT public: explicit SceneLoader(ViewportWindow* viewport, QObject* parent = nullptr); ~SceneLoader(); + // Sidecar cache use is opt-in per direction. Embedders that don't care + // about .ifcview can leave both off (default) and SceneLoader will never + // probe for or produce one. Toggles only affect *subsequent* loads; + // a load already in flight finishes with whatever was set when it + // started. Opening a `.ifcview` file directly always reads it, + // regardless of these flags. + void setShouldReadSidecar(bool enabled); + void setShouldWriteSidecar(bool enabled); + bool shouldReadSidecar() const { return should_read_sidecar_; } + bool shouldWriteSidecar() const { return should_write_sidecar_; } + // Returns the model_ids assigned to the enqueued paths, in order. // Callers can use these to set up per-model UI state (tree roots, etc.) // before any load signal fires. @@ -146,9 +160,18 @@ private: // streamer's first InstanceChunk. Eigen::Matrix4d first_placement = Eigen::Matrix4d::Identity(); bool has_first_placement = false; + + // Live-load sidecar accumulator. Constructed at the start of a + // stream load when shouldWriteSidecar is on; null otherwise. + std::unique_ptr sidecar_builder; + // Element batches accumulated as the streamer yields, mirrored from + // what's emitted via streamedElementsReady so finalize() has the + // full set without re-draining. + std::vector streamed_elements; }; void startNextLoad(); + void startStreamLoadFor(uint32_t mid); void connectStreamer(GeometryStreamer* streamer); void joinSidecarThread(); void joinDataSourceThreads(); @@ -156,6 +179,8 @@ private: void startDataSourceLoad(uint32_t mid); ViewportWindow* viewport_ = nullptr; + bool should_read_sidecar_ = false; + bool should_write_sidecar_ = false; std::map models_; std::deque load_queue_; uint32_t next_model_id_ = 1; diff --git a/src/ifcviewer/HeadlessSidecarBuilder.cpp b/src/ifcviewer/SidecarBuilder.cpp similarity index 83% rename from src/ifcviewer/HeadlessSidecarBuilder.cpp rename to src/ifcviewer/SidecarBuilder.cpp index c3bef7fdbe..82687fa917 100644 --- a/src/ifcviewer/HeadlessSidecarBuilder.cpp +++ b/src/ifcviewer/SidecarBuilder.cpp @@ -17,10 +17,9 @@ * * ********************************************************************************/ -#include "HeadlessSidecarBuilder.h" +#include "SidecarBuilder.h" #include "Federation.h" -#include "GeometryStreamer.h" #include "LodBuilder.h" #include "SidecarCache.h" #include "VertexQuantization.h" @@ -31,13 +30,14 @@ #include #include +#include -HeadlessSidecarBuilder::HeadlessSidecarBuilder(QObject* parent) +SidecarBuilder::SidecarBuilder(QObject* parent) : QObject(parent) { } -void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) { +void SidecarBuilder::onMeshReady(const MeshChunk& chunk) { if (chunk.vertices.empty() || chunk.indices.empty()) return; // Streamer format: 7 floats/vertex (pos3 + normal3 + color-as-float). @@ -45,7 +45,7 @@ void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) { // Recompute a tight local AABB from the actual vertex positions, same // way ViewportWindow::uploadMeshChunk does so the .ifcview byte layout - // matches the GPU-readback path. + // matches the live-render path. float bmin[3] = { std::numeric_limits::infinity(), std::numeric_limits::infinity(), std::numeric_limits::infinity() }; @@ -98,7 +98,7 @@ void HeadlessSidecarBuilder::onMeshReady(const MeshChunk& chunk) { sidecar_data_.meshes[chunk.local_mesh_id] = info; } -void HeadlessSidecarBuilder::onInstanceReady(const InstanceChunk& chunk) { +void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) { InstanceCpu inst; inst.mesh_id = chunk.local_mesh_id; inst.object_id = chunk.object_id; @@ -121,44 +121,8 @@ void HeadlessSidecarBuilder::onInstanceReady(const InstanceChunk& chunk) { sidecar_data_.instances.push_back(inst); } -bool HeadlessSidecarBuilder::build(const QString& ifc_path, - const QString& anchor_path, - int num_threads) { - sidecar_data_ = SidecarData{}; - last_error_.clear(); - - // Streamer lives on the calling thread; its worker_thread_ is its own - // internal QThread. AutoConnection routes meshReady/instanceReady - // through our local event loop. - GeometryStreamer streamer; - - QEventLoop loop; - bool failed = false; - - connect(&streamer, &GeometryStreamer::meshReady, - this, &HeadlessSidecarBuilder::onMeshReady); - connect(&streamer, &GeometryStreamer::instanceReady, - this, &HeadlessSidecarBuilder::onInstanceReady); - connect(&streamer, &GeometryStreamer::finished, - &loop, &QEventLoop::quit); - connect(&streamer, &GeometryStreamer::cancelled, - &loop, &QEventLoop::quit); - connect(&streamer, &GeometryStreamer::errorOccurred, this, - [&](const QString& msg) { - last_error_ = msg; - failed = true; - loop.quit(); - }); - - streamer.loadFile(ifc_path.toStdString(), - /*start_object_id*/ 1, - /*model_id*/ 1, - num_threads); - - loop.exec(); - - if (failed) return false; - +SidecarData SidecarBuilder::finalize(const ModelGeoref& georef, + const std::vector& elements) { // Per-mesh instance_count, matching ViewportWindow::finalizeModel. for (auto& mesh : sidecar_data_.meshes) { mesh.first_instance = 0; @@ -170,18 +134,13 @@ bool HeadlessSidecarBuilder::build(const QString& ifc_path, } } - // CoordinateOperation cache from the IFC the streamer just parsed. - if (auto* file = streamer.ifcFile()) { - ModelGeoref georef = computeModelGeoref(file); - sidecar_data_.has_coordinate_operation = georef.has_coordinate_operation ? 1 : 0; - Eigen::Map>( - sidecar_data_.coordinate_operation_meters) = georef.coordinate_operation_meters; - sidecar_data_.project_length_to_meters = georef.units.project_length_to_meters; - sidecar_data_.map_unit_to_meters = georef.units.map_unit_to_meters; - } + sidecar_data_.has_coordinate_operation = georef.has_coordinate_operation ? 1 : 0; + Eigen::Map>( + sidecar_data_.coordinate_operation_meters) = georef.coordinate_operation_meters; + sidecar_data_.project_length_to_meters = georef.units.project_length_to_meters; + sidecar_data_.map_unit_to_meters = georef.units.map_unit_to_meters; - // Element metadata accumulated by the streamer's worker thread. - for (const auto& info : streamer.drainElements()) { + for (const auto& info : elements) { PackedElementInfo packed; packed.object_id = info.object_id; packed.model_id = info.model_id; @@ -205,7 +164,51 @@ bool HeadlessSidecarBuilder::build(const QString& ifc_path, buildLods(sidecar_data_); - if (!writeSidecar(anchor_path.toStdString(), sidecar_data_)) { + return std::exchange(sidecar_data_, SidecarData{}); +} + +bool SidecarBuilder::build(const QString& ifc_path, + const QString& anchor_path, + int num_threads) { + sidecar_data_ = SidecarData{}; + last_error_.clear(); + + GeometryStreamer streamer; + QEventLoop loop; + bool failed = false; + + connect(&streamer, &GeometryStreamer::meshReady, + this, &SidecarBuilder::onMeshReady); + connect(&streamer, &GeometryStreamer::instanceReady, + this, &SidecarBuilder::onInstanceReady); + connect(&streamer, &GeometryStreamer::finished, + &loop, &QEventLoop::quit); + connect(&streamer, &GeometryStreamer::cancelled, + &loop, &QEventLoop::quit); + connect(&streamer, &GeometryStreamer::errorOccurred, this, + [&](const QString& msg) { + last_error_ = msg; + failed = true; + loop.quit(); + }); + + streamer.loadFile(ifc_path.toStdString(), + /*start_object_id*/ 1, + /*model_id*/ 1, + num_threads); + + loop.exec(); + + if (failed) return false; + + ModelGeoref georef; + if (auto* file = streamer.ifcFile()) { + georef = computeModelGeoref(file); + } + + SidecarData data = finalize(georef, streamer.drainElements()); + + if (!writeSidecar(anchor_path.toStdString(), data)) { last_error_ = "writeSidecar failed"; return false; } diff --git a/src/ifcviewer/HeadlessSidecarBuilder.h b/src/ifcviewer/SidecarBuilder.h similarity index 52% rename from src/ifcviewer/HeadlessSidecarBuilder.h rename to src/ifcviewer/SidecarBuilder.h index c330f5aac6..bb12d20f2a 100644 --- a/src/ifcviewer/HeadlessSidecarBuilder.h +++ b/src/ifcviewer/SidecarBuilder.h @@ -17,44 +17,63 @@ * * ********************************************************************************/ -#ifndef HEADLESSSIDECARBUILDER_H -#define HEADLESSSIDECARBUILDER_H +#ifndef SIDECARBUILDER_H +#define SIDECARBUILDER_H +#include "Federation.h" +#include "GeometryStreamer.h" #include "InstancedGeometry.h" #include "SidecarCache.h" #include #include -// Produces a .ifcview sidecar from an IFC file without touching the -// ViewportWindow or any GL state. Mirrors the data path that -// SceneLoader + ViewportWindow + ModelsPanelController.writeSidecarForModel -// take for live loads, but does the vertex quantization and SidecarData -// assembly entirely on the CPU. +#include + +// Assembles a .ifcview SidecarData from streamer output, then finalizes it +// (LOD build, georef, packed elements) ready for writeSidecar() and/or +// ViewportWindow::applyLodExtension(). Two use modes: // -// Threading: call ::build() from a non-GUI worker thread that has a Qt -// event dispatcher (e.g. the thread spawned by QThread::create). build() -// spins a local QEventLoop until the streamer's worker thread completes. -class HeadlessSidecarBuilder : public QObject { +// 1. Live load — host (SceneLoader) drives its own GeometryStreamer and +// forwards meshReady/instanceReady chunks via onMeshReady/onInstanceReady +// while the viewport also consumes them. When the stream finishes the +// host calls finalize(georef, elements) and writeSidecar() with the +// returned data. No GPU readback involved. +// +// 2. Offline — build() owns a streamer, runs it on a non-GUI worker thread, +// and writes the sidecar to disk. Used by the .rdbview export path. +class SidecarBuilder : public QObject { Q_OBJECT public: - explicit HeadlessSidecarBuilder(QObject* parent = nullptr); + explicit SidecarBuilder(QObject* parent = nullptr); - // `anchor_path` is fed to writeSidecar(), which normalises it to - // `.ifcview`. Pass either the IFC path itself - // (sidecar lands beside it) or a temp path whose stem you control. + // Convenience for the offline path: construct an internal streamer, + // accumulate, finalize, and write to disk. anchor_path is normalised to + // .ifcview by writeSidecar. Call from a non-GUI worker thread with + // a Qt event dispatcher; build() spins a local QEventLoop until the + // streamer's worker thread completes. bool build(const QString& ifc_path, const QString& anchor_path, int num_threads = 0); - const QString& lastError() const { return last_error_; } - -private: + // Accumulator interface. Safe to call repeatedly from the same thread the + // streamer signals are delivered to. void onMeshReady(const MeshChunk& chunk); void onInstanceReady(const InstanceChunk& chunk); + // Finishes assembly using the georef + element batch the host collected + // during streaming. Returns the assembled SidecarData by move; the + // builder's internal state is left empty so the same instance can be + // reused for another load. + SidecarData finalize(const ModelGeoref& georef, + const std::vector& elements); + + const QString& lastError() const { return last_error_; } + +private: SidecarData sidecar_data_; QString last_error_; }; -#endif // HEADLESSSIDECARBUILDER_H +#endif // SIDECARBUILDER_H + diff --git a/src/ifcviewer/VertexQuantization.h b/src/ifcviewer/VertexQuantization.h index 0b47f7528a..248dca2b35 100644 --- a/src/ifcviewer/VertexQuantization.h +++ b/src/ifcviewer/VertexQuantization.h @@ -20,9 +20,8 @@ // Inline helpers that turn streamer-format vertices (7 floats per vertex: // pos3 + normal3 + color-as-float) into the 12 B quantized VBO layout used // by both the viewport's GPU buffers and the .ifcview sidecar. Shared -// between ViewportWindow::uploadMeshChunk and HeadlessSidecarBuilder so -// the on-disk format stays identical to what the viewer would have -// produced via the GPU readback path. +// between ViewportWindow::uploadMeshChunk and SidecarBuilder so the +// on-disk format stays identical to what the viewport renders. #ifndef VERTEXQUANTIZATION_H #define VERTEXQUANTIZATION_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 1efcd31577..d3448006f3 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1187,28 +1187,6 @@ void ViewportWindow::finalizeModel(uint32_t model_id) { m.ssbo_capacity / (1024.0*1024.0)); } -bool ViewportWindow::snapshotModel(uint32_t model_id, SidecarData& out) const { - auto it = models_gpu_.find(model_id); - if (!gl_ || it == models_gpu_.end()) return false; - const auto& m = it->second; - if (!m.finalized) return false; - - // GPU readback of the packed VBO/EBO ranges actually in use. VBO is - // raw bytes at the quantized layout. - if (m.vbo_used > 0) { - out.vertices.resize(m.vbo_used); - gl_->glGetNamedBufferSubData(m.vbo, 0, m.vbo_used, out.vertices.data()); - } - if (m.ebo_used > 0) { - out.indices.resize(m.ebo_used / sizeof(uint32_t)); - gl_->glGetNamedBufferSubData(m.ebo, 0, m.ebo_used, out.indices.data()); - } - - out.meshes = m.meshes; - out.instances = m.instances; - return true; -} - void ViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) { if (!gl_initialized_) { PendingOperation op; diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index c464a7abf3..c05ee1c493 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -161,12 +161,6 @@ public: void resetScene(); - // Snapshot the finalised model into a SidecarData struct for caching. - // Vertices + indices are read back from the GPU; meshes/instances come - // from the CPU-side vectors. Leaves `elements` and `string_table` empty - // for the caller to fill in. - bool snapshotModel(uint32_t model_id, SidecarData& out) const; - // Restore a finalised model from a cached SidecarData struct. Replaces // any existing state for model_id and marks it drawable. void applyCachedModel(uint32_t model_id, SidecarData data);