mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 18:12:28 +00:00
Surface VRAM shortfall to the user and let them unload models
When the geometry in view needs more GPU memory than the cache can hold, the viewer keeps the largest on-screen chunks resident and streams the rest as the camera moves. That is the right degradation, but it was invisible: nothing told the user the scene did not fit, and the only lever was removing or hiding models, neither of which is "keep it in the federation but stop spending GPU memory on it". Viewer core: - ModelGpuData::unloaded, with drawable() = !hidden && !unloaded now the test every cull / draw / pick / streaming pass uses. unloadModel evicts every chunk and releases the model's own buffers; loadModel recreates them from the CPU mirrors (no disk read) and lets chunks stream back. Recompose keeps the CPU instances current while a model is unloaded so a reload sees up-to-date transforms. The MeshGpu/InstanceGpu record builders are factored out so load and reload share them. - FrameStats reports the camera's working set: chunks wanted, how many of those are not resident, and their bytes. - modelVramBytes / isModelUnloaded accessors, forwarded by ViewportWindow. BonsaiViewer: - Models tree gains a memory column (name | MB | eye) refreshed once a second and on load-state changes; unloaded models read "unloaded" in italics. The viewport stays the single authority for the state; SessionState only carries the modelLoadStateChanged notification. - Context menu: "Unload Model" / "Load Model", distinct from hide and remove, reporting the MB freed in the status bar. - Status bar notice, independent of the perf-stats toggle, once the shortfall has persisted for 3 s (a moment of missing chunks after any camera move is normal): "GPU memory full: N of M visible chunks (X MB) not loaded", with a tooltip pointing at Unload. The perf label also shows "N/M chunks waiting". Verified on the GPU: unloading a 497 MB model frees it immediately with the others still rendering; reloading streams all 180 chunks back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -428,7 +428,8 @@ void MainWindow::setupPanels() {
|
||||
spatial_panel_ = new modules::spatial_hierarchy::SpatialHierarchyPanel(this);
|
||||
properties_panel_ = new modules::properties::PropertiesPanel(this);
|
||||
|
||||
models_view_ = new modules::models::ModelsPanelView(models_panel_, session_state_, this);
|
||||
models_view_ = new modules::models::ModelsPanelView(
|
||||
models_panel_, session_state_, viewport_widget_->viewport(), this);
|
||||
spatial_view_ = new modules::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
|
||||
properties_view_ = new modules::properties::PropertiesPanelView(properties_panel_, session_state_, this);
|
||||
|
||||
@@ -481,6 +482,13 @@ void MainWindow::setupStatus() {
|
||||
status_mode_label_ = new QLabel("Ready", this);
|
||||
status_selection_label_ = new QLabel("No selection", this);
|
||||
status_perf_label_ = new QLabel(this);
|
||||
status_memory_label_ = new QLabel(this);
|
||||
status_memory_label_->setVisible(false);
|
||||
status_memory_label_->setToolTip(
|
||||
"The geometry in view needs more GPU memory than is available, so the "
|
||||
"viewer keeps the largest on-screen parts resident and streams the rest "
|
||||
"as you move. Right-click a model in the Models panel and choose "
|
||||
"\"Unload Model\" to free its GPU memory for the others.");
|
||||
status_progress_bar_ = new QProgressBar(this);
|
||||
status_perf_label_->setVisible(AppSettings::instance().showStats());
|
||||
status_progress_bar_->setMaximumWidth(200);
|
||||
@@ -489,6 +497,7 @@ void MainWindow::setupStatus() {
|
||||
statusBar()->setSizeGripEnabled(false);
|
||||
statusBar()->addWidget(status_mode_label_);
|
||||
statusBar()->addWidget(status_selection_label_, 1);
|
||||
statusBar()->addPermanentWidget(status_memory_label_);
|
||||
statusBar()->addPermanentWidget(status_perf_label_);
|
||||
statusBar()->addPermanentWidget(status_progress_bar_);
|
||||
|
||||
@@ -554,8 +563,28 @@ void MainWindow::setupLoader() {
|
||||
|
||||
connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
|
||||
[this](const ViewportWindow::FrameStats& stats) {
|
||||
if (!status_perf_label_->isVisible()) return;
|
||||
const double mb = 1.0 / (1024.0 * 1024.0);
|
||||
|
||||
// Missing chunks are normal for a moment after every camera move
|
||||
// while streaming catches up; only a shortfall that persists means
|
||||
// the view does not fit, and only that is worth telling the user.
|
||||
constexpr qint64 kShortfallNoticeMs = 3000;
|
||||
if (stats.chunks_wanted_missing == 0) {
|
||||
memory_shortfall_since_.invalidate();
|
||||
status_memory_label_->setVisible(false);
|
||||
} else {
|
||||
if (!memory_shortfall_since_.isValid()) memory_shortfall_since_.start();
|
||||
if (memory_shortfall_since_.elapsed() >= kShortfallNoticeMs) {
|
||||
status_memory_label_->setText(
|
||||
QString("GPU memory full: %1 of %2 visible chunks (%3 MB) not loaded")
|
||||
.arg(stats.chunks_wanted_missing)
|
||||
.arg(stats.chunks_wanted)
|
||||
.arg(double(stats.wanted_missing_bytes) * mb, 0, 'f', 0));
|
||||
status_memory_label_->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!status_perf_label_->isVisible()) return;
|
||||
QString text =
|
||||
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws | VRAM %8/%9 MB")
|
||||
.arg(stats.fps, 0, 'f', 1)
|
||||
@@ -581,6 +610,11 @@ void MainWindow::setupLoader() {
|
||||
.arg(double(stats.device_vram_used_bytes) * mb, 0, 'f', 0)
|
||||
.arg(double(stats.device_vram_total_bytes) * mb, 0, 'f', 0);
|
||||
}
|
||||
if (stats.chunks_wanted_missing > 0) {
|
||||
text += QString(" | %1/%2 chunks waiting")
|
||||
.arg(stats.chunks_wanted_missing)
|
||||
.arg(stats.chunks_wanted);
|
||||
}
|
||||
status_perf_label_->setText(text);
|
||||
});
|
||||
connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked,
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <QStringList>
|
||||
|
||||
class QLabel;
|
||||
#include <QElapsedTimer>
|
||||
class QDockWidget;
|
||||
class QMenu;
|
||||
class QProgressBar;
|
||||
@@ -70,6 +71,10 @@ private:
|
||||
QLabel* status_mode_label_ = nullptr;
|
||||
QLabel* status_selection_label_ = nullptr;
|
||||
QLabel* status_perf_label_ = nullptr;
|
||||
// Shown while the visible geometry persistently exceeds what fits in
|
||||
// GPU memory (see onFrameStats): the user's cue to unload models.
|
||||
QLabel* status_memory_label_ = nullptr;
|
||||
QElapsedTimer memory_shortfall_since_;
|
||||
QProgressBar* status_progress_bar_ = nullptr;
|
||||
bonsaiviewer::components::TabBar* ribbon_tabs_ = nullptr;
|
||||
QStackedWidget* ribbon_pages_ = nullptr;
|
||||
|
||||
@@ -199,6 +199,10 @@ void SessionState::notifyModelGeometryReady(uint32_t session_model_id) {
|
||||
emit modelGeometryReady(session_model_id);
|
||||
}
|
||||
|
||||
void SessionState::notifyModelLoadStateChanged(const QString& model_id) {
|
||||
emit modelLoadStateChanged(model_id);
|
||||
}
|
||||
|
||||
void SessionState::notifyProjectOpened(const QString& path) {
|
||||
emit projectOpened(path);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ public:
|
||||
void notifyFederationChanged();
|
||||
void notifyVisibilityChanged();
|
||||
void notifyModelGeometryReady(uint32_t session_model_id);
|
||||
void notifyModelLoadStateChanged(const QString& model_id);
|
||||
void notifyProjectOpened(const QString& path);
|
||||
void notifyProjectSaved(const QString& path);
|
||||
void notifyProjectReset();
|
||||
@@ -107,6 +108,10 @@ 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 session_model_id);
|
||||
// Fires when a model was unloaded from, or loaded back onto, the GPU
|
||||
// (commands::unloadModel / loadModel). The viewport is the authority
|
||||
// for the state itself — ViewportWindow::isModelUnloaded.
|
||||
void modelLoadStateChanged(const QString& model_id);
|
||||
// Fires when a model's live IFC data source (the .ifc/.rdb, opened in the
|
||||
// background after a sidecar-cache hit) becomes available for queries —
|
||||
// e.g. so the spatial hierarchy can be built once the file is loaded.
|
||||
|
||||
@@ -262,6 +262,28 @@ void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host,
|
||||
session.setStatusMessage("Models", "Model removed");
|
||||
}
|
||||
|
||||
void unloadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id) {
|
||||
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
|
||||
if (session_model_id == 0) return;
|
||||
if (session.loader()->isLoadingModel(session_model_id)) return;
|
||||
const double freed_mb = double(viewport.modelVramBytes(session_model_id)) / (1024.0 * 1024.0);
|
||||
viewport.unloadModel(session_model_id);
|
||||
session.notifyModelLoadStateChanged(model_id);
|
||||
session.setStatusMessage("Models", QString("Model unloaded (freed %1 MB of GPU memory)")
|
||||
.arg(freed_mb, 0, 'f', 0));
|
||||
}
|
||||
|
||||
void loadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id) {
|
||||
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
|
||||
if (session_model_id == 0) return;
|
||||
if (!viewport.loadModel(session_model_id)) {
|
||||
session.setStatusMessage("Models", "Not enough GPU memory to load this model");
|
||||
return;
|
||||
}
|
||||
session.notifyModelLoadStateChanged(model_id);
|
||||
session.setStatusMessage("Models", "Model loaded");
|
||||
}
|
||||
|
||||
void viewModels(SessionState& session, ViewportWindow& viewport, const QStringList& model_ids) {
|
||||
// Federation ids are the panel's currency; the viewport speaks session
|
||||
// model ids. sessionModelIdForModelId returns 0 for a model the viewport
|
||||
|
||||
@@ -60,6 +60,12 @@ void moveGroup(SessionState& session, const QString& id, const QString& parent_g
|
||||
void moveModels(SessionState& session, const QStringList& ids, const QString& parent_group_id);
|
||||
void removeGroup(SessionState& session, QWidget& host, const QString& group_id);
|
||||
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& model_id);
|
||||
// GPU residency, distinct from visibility (hide) and from membership
|
||||
// (remove): unloadModel frees everything the model holds on the device
|
||||
// while it stays in the federation; loadModel brings it back. Both emit
|
||||
// modelLoadStateChanged.
|
||||
void unloadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id);
|
||||
void loadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id);
|
||||
// "View Selected Model" — frame the camera on just these models' geometry, the
|
||||
// way View All frames the whole federation. Models that carry no loaded
|
||||
// geometry (never loaded, or still streaming their metadata) contribute
|
||||
|
||||
@@ -25,16 +25,25 @@
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QFont>
|
||||
#include <QColor>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
QStandardItem* siblingItem(QStandardItem* name_item, Column column) {
|
||||
QStandardItem* parent = name_item->parent();
|
||||
if (!parent) parent = name_item->model()->invisibleRootItem();
|
||||
return parent->child(name_item->row(), 1);
|
||||
return parent->child(name_item->row(), int(column));
|
||||
}
|
||||
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
return siblingItem(name_item, VisibilityColumn);
|
||||
}
|
||||
|
||||
QString formatMegabytes(quint64 bytes) {
|
||||
return QString("%1 MB").arg(double(bytes) / (1024.0 * 1024.0), 0, 'f', 0);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
@@ -51,7 +60,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
|
||||
: QStandardItemModel(parent)
|
||||
, federation_(federation)
|
||||
{
|
||||
setColumnCount(2);
|
||||
setColumnCount(ColumnCount);
|
||||
rebuildAll();
|
||||
|
||||
connect(federation_, &Federation::groupAdded, this, &FederationItemModel::onGroupAdded);
|
||||
@@ -67,7 +76,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
|
||||
|
||||
void FederationItemModel::rebuildAll() {
|
||||
clear();
|
||||
setColumnCount(2);
|
||||
setColumnCount(ColumnCount);
|
||||
id_to_name_item_.clear();
|
||||
|
||||
for (const auto& root_group : federation_->rootGroups()) {
|
||||
@@ -121,6 +130,14 @@ QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visib
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeMemoryItem() const {
|
||||
auto* item = new QStandardItem(QString());
|
||||
item->setEditable(false);
|
||||
item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
item->setForeground(QBrush(QColor(bonsaiviewer::ViewerSettings::instance().color("disabled_text"))));
|
||||
return item;
|
||||
}
|
||||
|
||||
void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visible) const {
|
||||
QStandardItem* vis_item = siblingVisibilityItem(name_item);
|
||||
if (visible) {
|
||||
@@ -133,6 +150,22 @@ void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visi
|
||||
}
|
||||
}
|
||||
|
||||
void FederationItemModel::setModelResidency(const QString& model_id, bool unloaded, quint64 vram_bytes) {
|
||||
QStandardItem* name_item = findItem(model_id);
|
||||
if (!name_item) return;
|
||||
QStandardItem* memory_item = siblingItem(name_item, MemoryColumn);
|
||||
if (!memory_item) return;
|
||||
const QString text = unloaded ? QStringLiteral("unloaded")
|
||||
: vram_bytes > 0 ? formatMegabytes(vram_bytes)
|
||||
: QString();
|
||||
if (memory_item->text() != text) memory_item->setText(text);
|
||||
QFont font = name_item->font();
|
||||
if (font.italic() != unloaded) {
|
||||
font.setItalic(unloaded);
|
||||
name_item->setFont(font);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::findItem(const QString& id) const {
|
||||
return id_to_name_item_.value(id, nullptr);
|
||||
}
|
||||
@@ -148,7 +181,7 @@ void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QStrin
|
||||
if (!model) return;
|
||||
auto* name_item = makeModelNameItem(model_id, model->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(model_id));
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
|
||||
id_to_name_item_.insert(model_id, name_item);
|
||||
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(model_id));
|
||||
}
|
||||
@@ -158,7 +191,7 @@ void FederationItemModel::appendGroupSubtreeTo(QStandardItem* parent_item, const
|
||||
if (!group) return;
|
||||
auto* name_item = makeGroupNameItem(group_id, group->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Group, group->visible);
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
|
||||
id_to_name_item_.insert(group_id, name_item);
|
||||
styleRowVisibility(name_item, group->visible);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class Federation;
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
// QStandardItemModel that mirrors the Federation tree (groups + models in
|
||||
// two columns: name + visibility icon). Subscribes directly to Federation's
|
||||
// three columns: name, GPU memory, visibility icon). Subscribes directly to Federation's
|
||||
// granular signals so each mutation only touches the affected rows — view
|
||||
// state (expansion, selection, scroll) is preserved automatically.
|
||||
//
|
||||
@@ -57,6 +57,12 @@ public:
|
||||
// previously- and newly-active model rows.
|
||||
void setActiveModelId(const QString& model_id);
|
||||
|
||||
// GPU residency is viewport state, not Federation state, so it is pushed
|
||||
// in by the owning View: the memory column shows `vram_bytes` for a
|
||||
// loaded model and "unloaded" for one the user unloaded (which is also
|
||||
// drawn in italics). Models the viewport knows nothing about show blank.
|
||||
void setModelResidency(const QString& model_id, bool unloaded, quint64 vram_bytes);
|
||||
|
||||
private slots:
|
||||
void onGroupAdded(const QString& group_id);
|
||||
void onGroupRemoved(const QString& group_id);
|
||||
@@ -72,6 +78,7 @@ private:
|
||||
QStandardItem* makeGroupNameItem(const QString& group_id, const QString& display_name) const;
|
||||
QStandardItem* makeModelNameItem(const QString& model_id, const QString& display_name) const;
|
||||
QStandardItem* makeVisibilityItem(ItemKind kind, bool visible) const;
|
||||
QStandardItem* makeMemoryItem() const;
|
||||
void styleRowVisibility(QStandardItem* name_item, bool visible) const;
|
||||
|
||||
QStandardItem* findItem(const QString& id) const;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDrag>
|
||||
@@ -79,6 +80,7 @@ QStringList selectedModelIdsAt(QTreeView* tree, const QModelIndex& clicked_index
|
||||
}
|
||||
|
||||
constexpr int kVisibilityColumnWidth = 28;
|
||||
constexpr int kMemoryColumnWidth = 72; // "1234 MB" / "unloaded"
|
||||
|
||||
// QTreeView subclass that handles drag-and-drop. Drop logic dispatches
|
||||
// through commands (not directly into the model) so notifications + status
|
||||
@@ -250,7 +252,7 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
|
||||
|
||||
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||
if (!index.isValid()) return;
|
||||
if (index.column() == 1) {
|
||||
if (index.column() == VisibilityColumn) {
|
||||
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
||||
return;
|
||||
}
|
||||
@@ -380,6 +382,24 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
|
||||
commands::saveModelAsToCloud(*session_state_, *this, id);
|
||||
});
|
||||
|
||||
// GPU residency. Unload keeps the model in the federation (and
|
||||
// its visibility) but frees everything it holds on the GPU — the
|
||||
// lever when the scene does not fit in VRAM. Load brings it back.
|
||||
menu.addSeparator();
|
||||
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(id);
|
||||
const bool unloaded = session_model_id != 0 && viewport_->isModelUnloaded(session_model_id);
|
||||
QAction* residency = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/cube.svg"),
|
||||
unloaded ? "Load Model" : "Unload Model");
|
||||
residency->setEnabled(session_model_id != 0);
|
||||
residency->setToolTip(unloaded
|
||||
? "Allocate GPU memory for this model again and stream its geometry back in."
|
||||
: "Free this model's GPU memory while keeping it in the federation.");
|
||||
connect(residency, &QAction::triggered, this, [this, id, unloaded]() {
|
||||
if (unloaded) commands::loadModel(*session_state_, *viewport_, id);
|
||||
else commands::unloadModel(*session_state_, *viewport_, id);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
QAction* remove = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
|
||||
@@ -409,14 +429,16 @@ void ModelsPanel::setModel(FederationItemModel* model) {
|
||||
}
|
||||
|
||||
void ModelsPanel::applyColumnLayout() {
|
||||
// Column 0 (name) stretches to fill; column 1 (visibility icon) is fixed.
|
||||
// The name stretches to fill; memory and visibility are fixed.
|
||||
QHeaderView* header = tree_->header();
|
||||
if (header->count() < 2) return;
|
||||
if (header->count() < ColumnCount) return;
|
||||
header->setStretchLastSection(false);
|
||||
header->setMinimumSectionSize(kVisibilityColumnWidth);
|
||||
header->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
header->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
header->resizeSection(1, kVisibilityColumnWidth);
|
||||
header->setSectionResizeMode(NameColumn, QHeaderView::Stretch);
|
||||
header->setSectionResizeMode(MemoryColumn, QHeaderView::Fixed);
|
||||
header->resizeSection(MemoryColumn, kMemoryColumnWidth);
|
||||
header->setSectionResizeMode(VisibilityColumn, QHeaderView::Fixed);
|
||||
header->resizeSection(VisibilityColumn, kVisibilityColumnWidth);
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
@@ -31,6 +31,14 @@ enum class ItemKind {
|
||||
Model,
|
||||
};
|
||||
|
||||
// Columns of the models tree: name | GPU memory | visibility eye.
|
||||
enum Column : int {
|
||||
NameColumn = 0,
|
||||
MemoryColumn = 1,
|
||||
VisibilityColumn = 2,
|
||||
ColumnCount = 3,
|
||||
};
|
||||
|
||||
struct TreeNode {
|
||||
QString id;
|
||||
QString name;
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
@@ -54,17 +57,19 @@ QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
bonsaiviewer::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
, model_(new FederationItemModel(session_state->federation(), this))
|
||||
{
|
||||
widget_->setModel(model_);
|
||||
|
||||
// Coarse signals: full rebuild + re-style. The granular Federation
|
||||
// signals are handled inside FederationItemModel and don't reach here.
|
||||
auto rebuild = [this]() { model_->rebuildAll(); };
|
||||
auto rebuild = [this]() { model_->rebuildAll(); refreshResidency(); };
|
||||
connect(session_state_, &SessionState::projectReset, this, rebuild);
|
||||
connect(session_state_, &SessionState::projectOpened, this, rebuild);
|
||||
connect(&bonsaiviewer::ViewerSettings::instance(),
|
||||
@@ -73,6 +78,30 @@ ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
connect(session_state_, &SessionState::activeModelChanged, this, [this](const QString& model_id) {
|
||||
model_->setActiveModelId(model_id);
|
||||
});
|
||||
|
||||
// Residency: immediately on the events that change it, and on a slow
|
||||
// tick for the memory figures, which move as chunks stream.
|
||||
auto refresh = [this]() { refreshResidency(); };
|
||||
connect(session_state_, &SessionState::modelLoadStateChanged, this, refresh);
|
||||
connect(session_state_, &SessionState::modelGeometryReady, this, refresh);
|
||||
connect(session_state_, &SessionState::modelsChanged, this, refresh);
|
||||
auto* tick = new QTimer(this);
|
||||
tick->setInterval(1000);
|
||||
connect(tick, &QTimer::timeout, this, refresh);
|
||||
tick->start();
|
||||
}
|
||||
|
||||
void ModelsPanelView::refreshResidency() {
|
||||
for (const auto& model : session_state_->federation()->models()) {
|
||||
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(model.id);
|
||||
if (session_model_id == 0) {
|
||||
model_->setModelResidency(model.id, false, 0);
|
||||
continue;
|
||||
}
|
||||
model_->setModelResidency(model.id,
|
||||
viewport_->isModelUnloaded(session_model_id),
|
||||
viewport_->modelVramBytes(session_model_id));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <QObject>
|
||||
|
||||
class Federation;
|
||||
class ViewportWindow;
|
||||
namespace bonsaiviewer { class SessionState; }
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
@@ -45,16 +46,25 @@ QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
// coarse session signals (project open/reset, theme change) — those are the
|
||||
// "rebuild from scratch" cases the model itself doesn't subscribe to.
|
||||
// Granular Federation events are handled inside the model.
|
||||
//
|
||||
// Also the bridge for the one thing the tree shows that is not Federation
|
||||
// state: each model's GPU residency (memory column, unloaded styling). The
|
||||
// viewport owns that state, so this view polls it once a second — the
|
||||
// numbers move continuously while geometry streams — and pushes it in.
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanelView(ModelsPanel* widget,
|
||||
bonsaiviewer::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void refreshResidency();
|
||||
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
bonsaiviewer::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
FederationItemModel* model_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user