ifcviewer: extract SceneLoader, remove duplicated load orchestration

MainWindow and MinimalWindow each carried ~150 lines of mirrored
load-queue, sidecar-thread, streamer-wiring, and ID-rebase code. Lift
all of it into a SceneLoader QObject in the library; both apps now
consume it via signals. Sidecar writes stay on the full-app side since
they need the consumer's element metadata strings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-04-21 18:05:19 +10:00
parent ae938d425b
commit 29f5132510
6 changed files with 582 additions and 560 deletions
+34 -170
View File
@@ -19,17 +19,10 @@
#include "MinimalWindow.h"
#include "AppSettings.h"
#include "SidecarCache.h"
#include <QApplication>
#include <QFileInfo>
#include <QStatusBar>
#include <QTimer>
#include <QDebug>
#include <memory>
#include <optional>
MinimalWindow::MinimalWindow(QWidget* parent)
: QMainWindow(parent)
{
@@ -45,6 +38,18 @@ MinimalWindow::MinimalWindow(QWidget* parent)
statusBar()->addWidget(status_label_, 1);
statusBar()->addPermanentWidget(stats_label_);
loader_ = new SceneLoader(viewport_, this);
connect(loader_, &SceneLoader::loadStarted,
this, &MinimalWindow::onLoadStarted);
connect(loader_, &SceneLoader::loadedFromSidecar,
this, &MinimalWindow::onLoadedFromSidecar);
connect(loader_, &SceneLoader::loadedFromStream,
this, &MinimalWindow::onLoadedFromStream);
connect(loader_, &SceneLoader::loadError,
this, &MinimalWindow::onLoadError);
connect(loader_, &SceneLoader::allLoadsFinished,
this, &MinimalWindow::onAllLoadsFinished);
connect(viewport_, &ViewportWindow::frameStatsUpdated, this,
[this](const ViewportWindow::FrameStats& s) {
if (!stats_label_->isVisible()) return;
@@ -65,187 +70,46 @@ MinimalWindow::MinimalWindow(QWidget* parent)
if (!show) stats_label_->clear();
});
// Periodically drain the streamer's pending-elements buffer so it doesn't
// grow unbounded on large models. We don't use the element data here —
// this window has no tree — but the buffer must still be flushed.
connect(&element_drain_timer_, &QTimer::timeout, this, &MinimalWindow::drainStreamerElements);
element_drain_timer_.setInterval(250);
setWindowTitle("IfcViewerMinimal");
resize(1200, 800);
}
MinimalWindow::~MinimalWindow() {
joinSidecarThread();
}
void MinimalWindow::joinSidecarThread() {
if (sidecar_read_thread_.joinable())
sidecar_read_thread_.join();
}
void MinimalWindow::addFiles(const QStringList& paths) {
for (const auto& path : paths) {
uint32_t id = next_model_id_++;
ModelEntry entry;
entry.id = id;
entry.file_path = path;
entry.display_name = QFileInfo(path).fileName();
entry.streamer = new GeometryStreamer(this);
models_[id] = entry;
load_queue_.push_back(id);
}
if (loading_model_id_ == 0) {
QTimer::singleShot(0, this, &MinimalWindow::startNextLoad);
}
loader_->addFiles(paths);
}
void MinimalWindow::connectStreamer(GeometryStreamer* streamer) {
connect(streamer, &GeometryStreamer::meshReady,
this, &MinimalWindow::onMeshReady, Qt::QueuedConnection);
connect(streamer, &GeometryStreamer::instanceReady,
this, &MinimalWindow::onInstanceReady, Qt::QueuedConnection);
connect(streamer, &GeometryStreamer::finished,
this, &MinimalWindow::onStreamingFinished, Qt::QueuedConnection);
connect(streamer, &GeometryStreamer::errorOccurred,
this, &MinimalWindow::onErrorOccurred, Qt::QueuedConnection);
}
void MinimalWindow::startNextLoad() {
if (load_queue_.empty()) {
loading_model_id_ = 0;
status_label_->setText(QString("Loaded %1 model(s)").arg(models_.size()));
applyPendingBenchmark();
return;
}
loading_model_id_ = load_queue_.front();
load_queue_.pop_front();
auto& model = models_[loading_model_id_];
load_timer_.restart();
status_label_->setText("Loading: " + model.display_name);
std::string ifc_path = model.file_path.toStdString();
uint64_t file_size = static_cast<uint64_t>(QFileInfo(model.file_path).size());
uint32_t mid = loading_model_id_;
joinSidecarThread();
sidecar_read_thread_ = std::thread([this, ifc_path, file_size, mid]() {
QElapsedTimer rt; rt.start();
auto cached = readSidecar(ifc_path, file_size);
qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached));
QMetaObject::invokeMethod(this, [this, mid, result]() {
if (*result && !(*result)->instances.empty()) {
applySidecarData(mid, std::move(**result));
} else {
auto it = models_.find(mid);
if (it == models_.end()) return;
auto& m = it->second;
connectStreamer(m.streamer);
element_drain_timer_.start();
m.streamer->loadFile(
m.file_path.toStdString(), next_object_id_, loading_model_id_);
}
}, Qt::QueuedConnection);
});
}
void MinimalWindow::applySidecarData(uint32_t mid, SidecarData data) {
auto it = models_.find(mid);
if (it == models_.end()) return;
auto& model = it->second;
qDebug("Sidecar hit: %s (%zu verts, %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());
// Rebase object/model IDs onto the current session's ID space. Matches
// MainWindow::applySidecarData — two cached models both starting at
// object_id=1 would collide otherwise.
uint32_t min_oid = UINT32_MAX;
for (const auto& pe : data.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) {
oid_offset = next_object_id_ - min_oid;
}
for (auto& pe : data.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) {
inst.object_id += oid_offset;
inst.model_id = mid;
}
viewport_->applyCachedModel(mid, std::move(data));
qint64 ms = load_timer_.elapsed();
QString elapsed = (ms >= 1000)
static QString formatElapsed(qint64 ms) {
return (ms >= 1000)
? QString::number(ms / 1000.0, 'f', 2) + " s"
: QString::number(ms) + " ms";
}
void MinimalWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) {
status_label_->setText("Loading: " + display_name);
}
void MinimalWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) {
status_label_->setText(QString("%1 loaded from cache in %2")
.arg(model.display_name).arg(elapsed));
loading_model_id_ = 0;
QTimer::singleShot(0, this, &MinimalWindow::startNextLoad);
.arg(loader_->displayName(mid))
.arg(formatElapsed(elapsed_ms)));
}
void MinimalWindow::onMeshReady(MeshChunk chunk) {
viewport_->uploadMeshChunk(chunk);
void MinimalWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) {
status_label_->setText(QString("%1 streamed in %2")
.arg(loader_->displayName(mid))
.arg(formatElapsed(elapsed_ms)));
}
void MinimalWindow::onInstanceReady(InstanceChunk chunk) {
viewport_->uploadInstanceChunk(chunk);
}
void MinimalWindow::drainStreamerElements() {
if (loading_model_id_ == 0) return;
auto it = models_.find(loading_model_id_);
if (it == models_.end()) return;
(void)it->second.streamer->drainElements();
}
void MinimalWindow::onStreamingFinished() {
element_drain_timer_.stop();
drainStreamerElements();
if (loading_model_id_ != 0) {
auto it = models_.find(loading_model_id_);
if (it != models_.end()) {
next_object_id_ = it->second.streamer->lastObjectId();
viewport_->finalizeModel(loading_model_id_);
}
}
qint64 ms = load_timer_.elapsed();
QString elapsed = (ms >= 1000)
? QString::number(ms / 1000.0, 'f', 2) + " s"
: QString::number(ms) + " ms";
auto it = models_.find(loading_model_id_);
QString name = (it != models_.end()) ? it->second.display_name : QString();
status_label_->setText(QString("%1 streamed in %2").arg(name).arg(elapsed));
startNextLoad();
}
void MinimalWindow::onErrorOccurred(const QString& message) {
void MinimalWindow::onLoadError(uint32_t /*mid*/, QString message) {
qWarning("IfcViewerMinimal error: %s", qPrintable(message));
status_label_->setText("Error: " + message);
}
void MinimalWindow::onAllLoadsFinished() {
status_label_->setText(QString("Loaded %1 model(s)").arg(loader_->modelCount()));
applyPendingBenchmark();
}
void MinimalWindow::setPendingCamera(const QString& params) {
pending_camera_ = params;
}
+8 -34
View File
@@ -22,62 +22,36 @@
#include <QMainWindow>
#include <QLabel>
#include <QTimer>
#include <QElapsedTimer>
#include <map>
#include <deque>
#include <thread>
#include "ViewportWindow.h"
#include "GeometryStreamer.h"
#include "SceneLoader.h"
class MinimalWindow : public QMainWindow {
Q_OBJECT
public:
explicit MinimalWindow(QWidget* parent = nullptr);
~MinimalWindow();
~MinimalWindow() = default;
void addFiles(const QStringList& paths);
void setPendingCamera(const QString& params);
void setPendingBenchmark(int frames);
private slots:
void onMeshReady(MeshChunk chunk);
void onInstanceReady(InstanceChunk chunk);
void onStreamingFinished();
void onErrorOccurred(const QString& message);
void drainStreamerElements();
void onLoadStarted(uint32_t mid, QString display_name);
void onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
void onLoadedFromStream(uint32_t mid, qint64 elapsed_ms);
void onLoadError(uint32_t mid, QString message);
void onAllLoadsFinished();
private:
struct ModelEntry {
uint32_t id = 0;
QString file_path;
QString display_name;
GeometryStreamer* streamer = nullptr;
};
void startNextLoad();
void connectStreamer(GeometryStreamer* streamer);
void joinSidecarThread();
void applySidecarData(uint32_t mid, SidecarData data);
void applyPendingBenchmark();
ViewportWindow* viewport_ = nullptr;
SceneLoader* loader_ = nullptr;
QWidget* viewport_container_ = nullptr;
QLabel* status_label_ = nullptr;
QLabel* stats_label_ = nullptr;
std::map<uint32_t, ModelEntry> models_;
std::deque<uint32_t> load_queue_;
uint32_t next_model_id_ = 1;
uint32_t next_object_id_ = 1;
uint32_t loading_model_id_ = 0;
std::thread sidecar_read_thread_;
QTimer element_drain_timer_;
QElapsedTimer load_timer_;
QString pending_camera_;
int pending_benchmark_ = 0;
};