ifcviewer: promote runtime perf knobs, drop always-on settings

Promotes five env-var-driven knobs to AppSettings + the settings dialog
(min pixel radius, motion min pixel radius, LOD1 pixel threshold, HiZ
resolution, HiZ on/off).  Defaults: motion min pixel radius is now 10
(was 0/disabled) and IFC_HIZ_MOTION is on by default — the strict
view-projection gate reverts via env var =0 when chasing HiZ
correctness bugs.  ViewportWindow connects each *Changed signal so
changes invalidate cached cull state and take effect on the next
frame.

Removes "Load Property Data Source" and "Apply Coordinate Operation"
from the settings dialog: both are now hardcoded on.  The basic-info
property fallback (used when there's no live IFC source for an object,
e.g. .ifcview without a sibling) now triggers organically when
ElementRegistry::findEntity returns null instead of being gated on a
user toggle.  Federation::guessFederatedFalseOrigin lost its
apply_coordinate_operation parameter and now uses
georef.has_coordinate_operation directly.

src/ifcviewer/settings.rst documents the remaining diagnostic env vars
(IFC_HIZ_MOTION, IFC_CULL_THREADS, IFC_SKIP_MDI, IFC_MAX_SUBDRAWS,
IFC_FPS_HITCH_MS, IFC_SUBDRAW_DIAG, IFC_LOD_*) plus a cross-walk from
the old promoted-knob env-var names to their new QSettings keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-10 19:37:20 +10:00
parent 7029feca0a
commit cf3f3c2c00
14 changed files with 324 additions and 185 deletions
+5 -19
View File
@@ -182,16 +182,6 @@ MainWindow::MainWindow(QWidget* parent)
if (!show) stats_label_->clear();
});
// Toggling the CoordinateOperation setting walks every loaded model
// and pushes either its georef matrix or identity to the viewport.
connect(&AppSettings::instance(),
&AppSettings::applyCoordinateOperationChanged,
this, [this](bool /*enabled*/) {
for (const auto& kv : fed_id_to_model_id_) {
applyCoordinateOperationToViewport(kv.second);
}
});
updateWindowTitle();
resize(1400, 900);
}
@@ -707,11 +697,9 @@ void MainWindow::onDataSourceReady(uint32_t mid) {
void MainWindow::applyCoordinateOperationToViewport(uint32_t mid) {
Eigen::Matrix4d M = Eigen::Matrix4d::Identity();
if (AppSettings::instance().applyCoordinateOperation()) {
if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
if (gr->has_coordinate_operation) {
M = gr->coordinate_operation_meters;
}
if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
if (gr->has_coordinate_operation) {
M = gr->coordinate_operation_meters;
}
}
viewport_->setModelCoordinateOperation(mid, M);
@@ -733,8 +721,7 @@ void MainWindow::applyModelTransformationToViewport(uint32_t mid) {
Eigen::Matrix4d coord_op = Eigen::Matrix4d::Identity();
if (const ModelGeoref* gr = loader_->modelGeoref(mid)) {
units = gr->units;
if (AppSettings::instance().applyCoordinateOperation() &&
gr->has_coordinate_operation) {
if (gr->has_coordinate_operation) {
coord_op = gr->coordinate_operation_meters;
}
}
@@ -771,8 +758,7 @@ void MainWindow::maybeGuessFederatedFalseOrigin(uint32_t mid) {
if (placement == nullptr || gr == nullptr) return;
const FederatedFalseOrigin guess = guessFederatedFalseOrigin(
*placement, *gr, federation_->config(),
AppSettings::instance().applyCoordinateOperation());
*placement, *gr, federation_->config());
federation_->setFederatedFalseOrigin(guess);
}
+54 -21
View File
@@ -52,21 +52,6 @@ void SettingsWindow::setupUi() {
"closed solids; disable if you see holes in open geometry.");
form->addRow("Backface Culling", backface_culling_check_);
load_data_source_check_ = new QCheckBox(this);
load_data_source_check_->setToolTip(
"Keep the .ifc/.rdb open after loading so element properties can "
"be queried. Disable for geometry-only viewing — saves memory "
"and, on sidecar hits, avoids a second file read.");
form->addRow("Load Property Data Source", load_data_source_check_);
apply_coordinate_operation_check_ = new QCheckBox(this);
apply_coordinate_operation_check_->setToolTip(
"Apply each model's IfcCoordinateOperation (e.g. IfcMapConversion) "
"after load so it lands in georeferenced map coordinates. "
"Disable to keep models in their local engineering frame.");
form->addRow("Apply Coordinate Operation",
apply_coordinate_operation_check_);
void_limit_spin_ = new QSpinBox(this);
void_limit_spin_->setRange(0, 100000);
void_limit_spin_->setToolTip(
@@ -94,6 +79,50 @@ void SettingsWindow::setupUi() {
"curved surface. Smaller = smoother shading but more triangles.");
form->addRow("Angular Tolerance", angular_tolerance_spin_);
min_pixel_radius_spin_ = new QDoubleSpinBox(this);
min_pixel_radius_spin_->setRange(0.0, 100.0);
min_pixel_radius_spin_->setDecimals(2);
min_pixel_radius_spin_->setSingleStep(0.5);
min_pixel_radius_spin_->setToolTip(
"Minimum projected sphere radius (in pixels) for an instance to "
"be drawn. Bigger = faster but more pop-in on small detail.");
form->addRow("Min Pixel Radius", min_pixel_radius_spin_);
motion_min_pixel_radius_spin_ = new QDoubleSpinBox(this);
motion_min_pixel_radius_spin_->setRange(0.0, 100.0);
motion_min_pixel_radius_spin_->setDecimals(2);
motion_min_pixel_radius_spin_->setSingleStep(1.0);
motion_min_pixel_radius_spin_->setToolTip(
"Aggressive cull threshold while the camera is moving. 0 = no "
"motion boost (motion uses the same threshold as still frames). "
"Big perceived FPS win on heavy scenes.");
form->addRow("Motion Min Pixel Radius", motion_min_pixel_radius_spin_);
lod1_pixel_threshold_spin_ = new QDoubleSpinBox(this);
lod1_pixel_threshold_spin_->setRange(0.0, 1000.0);
lod1_pixel_threshold_spin_->setDecimals(1);
lod1_pixel_threshold_spin_->setSingleStep(1.0);
lod1_pixel_threshold_spin_->setToolTip(
"Pixel radius below which an instance switches to its LOD1 "
"representation. 0 disables LOD1 entirely (always draw LOD0).");
form->addRow("LOD1 Pixel Threshold", lod1_pixel_threshold_spin_);
hiz_enabled_check_ = new QCheckBox(this);
hiz_enabled_check_->setToolTip(
"Enable HiZ (hierarchical Z) occlusion culling. Hides geometry "
"behind opaque blockers based on a downsampled depth pyramid "
"from the previous frame. Big perf win on dense interiors.");
form->addRow("HiZ Occlusion", hiz_enabled_check_);
hiz_resolution_spin_ = new QSpinBox(this);
hiz_resolution_spin_->setRange(64, 4096);
hiz_resolution_spin_->setSingleStep(64);
hiz_resolution_spin_->setToolTip(
"Base HiZ pyramid width in texels (height tracks aspect). "
"Bigger = tighter occlusion but more readback bandwidth. "
"Changes take effect on next viewport reinitialization.");
form->addRow("HiZ Resolution", hiz_resolution_spin_);
auto* button_box = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
@@ -116,23 +145,27 @@ void SettingsWindow::syncFromSettings() {
geometry_library_edit_->setText(AppSettings::instance().geometryLibrary());
show_stats_check_->setChecked(AppSettings::instance().showStats());
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
load_data_source_check_->setChecked(AppSettings::instance().loadDataSource());
apply_coordinate_operation_check_->setChecked(
AppSettings::instance().applyCoordinateOperation());
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance());
angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance());
min_pixel_radius_spin_->setValue(AppSettings::instance().minPixelRadius());
motion_min_pixel_radius_spin_->setValue(AppSettings::instance().motionMinPixelRadius());
lod1_pixel_threshold_spin_->setValue(AppSettings::instance().lod1PixelThreshold());
hiz_enabled_check_->setChecked(AppSettings::instance().hizEnabled());
hiz_resolution_spin_->setValue(AppSettings::instance().hizResolution());
}
void SettingsWindow::onAccepted() {
AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text());
AppSettings::instance().setShowStats(show_stats_check_->isChecked());
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
AppSettings::instance().setLoadDataSource(load_data_source_check_->isChecked());
AppSettings::instance().setApplyCoordinateOperation(
apply_coordinate_operation_check_->isChecked());
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value());
AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value());
AppSettings::instance().setMinPixelRadius(min_pixel_radius_spin_->value());
AppSettings::instance().setMotionMinPixelRadius(motion_min_pixel_radius_spin_->value());
AppSettings::instance().setLod1PixelThreshold(lod1_pixel_threshold_spin_->value());
AppSettings::instance().setHizEnabled(hiz_enabled_check_->isChecked());
AppSettings::instance().setHizResolution(hiz_resolution_spin_->value());
accept();
}
+5 -2
View File
@@ -46,11 +46,14 @@ private:
QLineEdit* geometry_library_edit_ = nullptr;
QCheckBox* show_stats_check_ = nullptr;
QCheckBox* backface_culling_check_ = nullptr;
QCheckBox* load_data_source_check_ = nullptr;
QCheckBox* apply_coordinate_operation_check_ = nullptr;
QSpinBox* void_limit_spin_ = nullptr;
QDoubleSpinBox* deflection_tolerance_spin_ = nullptr;
QDoubleSpinBox* angular_tolerance_spin_ = nullptr;
QDoubleSpinBox* min_pixel_radius_spin_ = nullptr;
QDoubleSpinBox* motion_min_pixel_radius_spin_ = nullptr;
QDoubleSpinBox* lod1_pixel_threshold_spin_ = nullptr;
QSpinBox* hiz_resolution_spin_ = nullptr;
QCheckBox* hiz_enabled_check_ = nullptr;
};
#endif
+85 -29
View File
@@ -26,14 +26,22 @@ constexpr const char* kGeometryLibraryKey = "geometry/library";
constexpr const char* kGeometryLibraryDefault = "hybrid-cgal-simple-opencascade";
constexpr const char* kShowStatsKey = "viewport/show_stats";
constexpr const char* kBackfaceCullingKey = "viewport/backface_culling";
constexpr const char* kLoadDataSourceKey = "loading/load_data_source";
constexpr const char* kApplyCoordinateOperationKey = "loading/apply_coordinate_operation";
constexpr const char* kVoidLimitKey = "loading/void_limit";
constexpr int kVoidLimitDefault = 30;
constexpr const char* kDeflectionToleranceKey = "loading/deflection_tolerance";
constexpr double kDeflectionToleranceDefault = 0.001;
constexpr const char* kAngularToleranceKey = "loading/angular_tolerance";
constexpr double kAngularToleranceDefault = 0.5;
constexpr const char* kMinPixelRadiusKey = "viewport/min_pixel_radius";
constexpr double kMinPixelRadiusDefault = 2.0;
constexpr const char* kMotionMinPixelRadiusKey = "viewport/motion_min_pixel_radius";
constexpr double kMotionMinPixelRadiusDefault = 10.0;
constexpr const char* kLod1PixelThresholdKey = "viewport/lod1_pixel_threshold";
constexpr double kLod1PixelThresholdDefault = 30.0;
constexpr const char* kHizResolutionKey = "viewport/hiz_resolution";
constexpr int kHizResolutionDefault = 256;
constexpr int kHizResolutionFloor = 64;
constexpr const char* kHizEnabledKey = "viewport/hiz_enabled";
}
AppSettings& AppSettings::instance() {
@@ -78,28 +86,6 @@ void AppSettings::setBackfaceCulling(bool value) {
emit backfaceCullingChanged(value);
}
bool AppSettings::loadDataSource() const {
return load_data_source_;
}
void AppSettings::setLoadDataSource(bool value) {
if (load_data_source_ == value) return;
load_data_source_ = value;
persist();
emit loadDataSourceChanged(value);
}
bool AppSettings::applyCoordinateOperation() const {
return apply_coordinate_operation_;
}
void AppSettings::setApplyCoordinateOperation(bool value) {
if (apply_coordinate_operation_ == value) return;
apply_coordinate_operation_ = value;
persist();
emit applyCoordinateOperationChanged(value);
}
int AppSettings::voidLimit() const {
return void_limit_;
}
@@ -136,20 +122,87 @@ void AppSettings::setAngularTolerance(double value) {
emit angularToleranceChanged(value);
}
double AppSettings::minPixelRadius() const {
return min_pixel_radius_;
}
void AppSettings::setMinPixelRadius(double value) {
if (value < 0.0) value = 0.0;
if (min_pixel_radius_ == value) return;
min_pixel_radius_ = value;
persist();
emit minPixelRadiusChanged(value);
}
double AppSettings::motionMinPixelRadius() const {
return motion_min_pixel_radius_;
}
void AppSettings::setMotionMinPixelRadius(double value) {
if (value < 0.0) value = 0.0;
if (motion_min_pixel_radius_ == value) return;
motion_min_pixel_radius_ = value;
persist();
emit motionMinPixelRadiusChanged(value);
}
double AppSettings::lod1PixelThreshold() const {
return lod1_pixel_threshold_;
}
void AppSettings::setLod1PixelThreshold(double value) {
if (value < 0.0) value = 0.0;
if (lod1_pixel_threshold_ == value) return;
lod1_pixel_threshold_ = value;
persist();
emit lod1PixelThresholdChanged(value);
}
int AppSettings::hizResolution() const {
return hiz_resolution_;
}
void AppSettings::setHizResolution(int value) {
if (value < kHizResolutionFloor) value = kHizResolutionFloor;
if (hiz_resolution_ == value) return;
hiz_resolution_ = value;
persist();
emit hizResolutionChanged(value);
}
bool AppSettings::hizEnabled() const {
return hiz_enabled_;
}
void AppSettings::setHizEnabled(bool value) {
if (hiz_enabled_ == value) return;
hiz_enabled_ = value;
persist();
emit hizEnabledChanged(value);
}
void AppSettings::load() {
QSettings settings;
geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString();
show_stats_ = settings.value(kShowStatsKey, false).toBool();
backface_culling_ = settings.value(kBackfaceCullingKey, true).toBool();
load_data_source_ = settings.value(kLoadDataSourceKey, true).toBool();
apply_coordinate_operation_ =
settings.value(kApplyCoordinateOperationKey, false).toBool();
void_limit_ = settings.value(kVoidLimitKey, kVoidLimitDefault).toInt();
if (void_limit_ < 0) void_limit_ = 0;
deflection_tolerance_ = settings.value(kDeflectionToleranceKey, kDeflectionToleranceDefault).toDouble();
if (deflection_tolerance_ <= 0.0) deflection_tolerance_ = kDeflectionToleranceDefault;
angular_tolerance_ = settings.value(kAngularToleranceKey, kAngularToleranceDefault).toDouble();
if (angular_tolerance_ <= 0.0) angular_tolerance_ = kAngularToleranceDefault;
min_pixel_radius_ = settings.value(kMinPixelRadiusKey, kMinPixelRadiusDefault).toDouble();
if (min_pixel_radius_ < 0.0) min_pixel_radius_ = 0.0;
motion_min_pixel_radius_ =
settings.value(kMotionMinPixelRadiusKey, kMotionMinPixelRadiusDefault).toDouble();
if (motion_min_pixel_radius_ < 0.0) motion_min_pixel_radius_ = 0.0;
lod1_pixel_threshold_ =
settings.value(kLod1PixelThresholdKey, kLod1PixelThresholdDefault).toDouble();
if (lod1_pixel_threshold_ < 0.0) lod1_pixel_threshold_ = 0.0;
hiz_resolution_ = settings.value(kHizResolutionKey, kHizResolutionDefault).toInt();
if (hiz_resolution_ < kHizResolutionFloor) hiz_resolution_ = kHizResolutionFloor;
hiz_enabled_ = settings.value(kHizEnabledKey, true).toBool();
}
void AppSettings::persist() {
@@ -157,9 +210,12 @@ void AppSettings::persist() {
settings.setValue(kGeometryLibraryKey, geometry_library_);
settings.setValue(kShowStatsKey, show_stats_);
settings.setValue(kBackfaceCullingKey, backface_culling_);
settings.setValue(kLoadDataSourceKey, load_data_source_);
settings.setValue(kApplyCoordinateOperationKey, apply_coordinate_operation_);
settings.setValue(kVoidLimitKey, void_limit_);
settings.setValue(kDeflectionToleranceKey, deflection_tolerance_);
settings.setValue(kAngularToleranceKey, angular_tolerance_);
settings.setValue(kMinPixelRadiusKey, min_pixel_radius_);
settings.setValue(kMotionMinPixelRadiusKey, motion_min_pixel_radius_);
settings.setValue(kLod1PixelThresholdKey, lod1_pixel_threshold_);
settings.setValue(kHizResolutionKey, hiz_resolution_);
settings.setValue(kHizEnabledKey, hiz_enabled_);
}
+42 -19
View File
@@ -40,21 +40,6 @@ public:
bool backfaceCulling() const;
void setBackfaceCulling(bool value);
// When true, the IFC/RocksDB file is kept open (and, on sidecar hits,
// opened in the background) so element properties can be queried.
// When false, only geometry is loaded — saves memory and avoids a
// second file read on sidecar hits, at the cost of no property panel.
bool loadDataSource() const;
void setLoadDataSource(bool value);
// When true, each loaded model's IfcCoordinateOperation (e.g.
// IfcMapConversion) is applied to the per-instance transform after
// load, lifting the model into map (georeferenced) coordinates.
// When false, models render in their local engineering frame —
// useful for previewing geometry without translating to e.g. UTM.
bool applyCoordinateOperation() const;
void setApplyCoordinateOperation(bool value);
// Skip elements with more than this many voids (HasOpenings inverse).
// Boolean subtraction of many openings is the dominant cost in some
// pathological exports; dropping those elements keeps load times sane.
@@ -72,15 +57,50 @@ public:
double angularTolerance() const;
void setAngularTolerance(double value);
// Minimum projected sphere radius (in pixels) for an instance to be
// worth drawing — below this it's contribution-culled. Bigger value
// = faster rendering but more pop-in on small detail; smaller =
// more thorough but more draw cost.
double minPixelRadius() const;
void setMinPixelRadius(double value);
// Aggressive contribution-cull threshold while the camera is moving.
// 0 disables the motion boost (motion uses minPixelRadius like still
// frames). When >= minPixelRadius, motion frames raise the bar so
// transient camera moves stay smooth on heavy scenes.
double motionMinPixelRadius() const;
void setMotionMinPixelRadius(double value);
// Projected sphere radius (in pixels) below which an instance
// switches to its LOD1 representation. 0 disables LOD1 entirely
// (always draw LOD0).
double lod1PixelThreshold() const;
void setLod1PixelThreshold(double value);
// Base HiZ pyramid width in texels (height tracks aspect). Bigger
// = tighter occlusion but more readback bandwidth. Floored at 64.
// Changes take effect on next viewport reinitialization.
int hizResolution() const;
void setHizResolution(int value);
// Master toggle for HiZ occlusion culling. When false, only the
// frustum + contribution cull run; geometry hidden behind opaque
// blockers still draws.
bool hizEnabled() const;
void setHizEnabled(bool value);
signals:
void geometryLibraryChanged(const QString& value);
void showStatsChanged(bool value);
void backfaceCullingChanged(bool value);
void loadDataSourceChanged(bool value);
void applyCoordinateOperationChanged(bool value);
void voidLimitChanged(int value);
void deflectionToleranceChanged(double value);
void angularToleranceChanged(double value);
void minPixelRadiusChanged(double value);
void motionMinPixelRadiusChanged(double value);
void lod1PixelThresholdChanged(double value);
void hizResolutionChanged(int value);
void hizEnabledChanged(bool value);
private:
AppSettings();
@@ -90,11 +110,14 @@ private:
QString geometry_library_;
bool show_stats_ = false;
bool backface_culling_ = true;
bool load_data_source_ = true;
bool apply_coordinate_operation_ = false;
int void_limit_ = 30;
double deflection_tolerance_ = 0.001;
double angular_tolerance_ = 0.5;
double min_pixel_radius_ = 2.0;
double motion_min_pixel_radius_ = 10.0;
double lod1_pixel_threshold_ = 30.0;
int hiz_resolution_ = 256;
bool hiz_enabled_ = true;
};
#endif // APPSETTINGS_H
+2 -4
View File
@@ -140,12 +140,10 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file) {
FederatedFalseOrigin
guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters,
const ModelGeoref& georef,
const FederationConfig& fed_cfg,
bool apply_coordinate_operation) {
const FederationConfig& fed_cfg) {
Eigen::Vector3d t_m = first_placement_meters.block<3, 1>(0, 3);
const bool use_coord_op =
apply_coordinate_operation && georef.has_coordinate_operation;
const bool use_coord_op = georef.has_coordinate_operation;
if (use_coord_op) {
const Eigen::Vector4d th(t_m.x(), t_m.y(), t_m.z(), 1.0);
t_m = (georef.coordinate_operation_meters * th).head<3>();
+7 -8
View File
@@ -135,18 +135,17 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file);
// Position: `first_placement_meters` is the model's "anchor" placement —
// typically the first instance's `placement_transformation`, which the
// iterator already produces in metres (its `convert-back-units` default
// is false). The translation is optionally lifted through
// `georef.coordinate_operation_meters` (controlled by
// `apply_coordinate_operation`), then expressed in the federation unit.
// is false). The translation is lifted through
// `georef.coordinate_operation_meters` when one is present, then
// expressed in the federation unit.
//
// Rotation: read directly from `georef.coordinate_operation_meters` when
// `apply_coordinate_operation && has_coordinate_operation` (this is the
// helmert grid-north angle). Otherwise zero. Anticlockwise positive.
// Rotation: read directly from `georef.coordinate_operation_meters`
// when `has_coordinate_operation` (this is the helmert grid-north
// angle); otherwise zero. Anticlockwise positive.
FederatedFalseOrigin
guessFederatedFalseOrigin(const Eigen::Matrix4d& first_placement_meters,
const ModelGeoref& georef,
const FederationConfig& fed_cfg,
bool apply_coordinate_operation);
const FederationConfig& fed_cfg);
// 1 federation_unit -> N metres.
double federationUnitToMeters(const FederationConfig&);
+1 -10
View File
@@ -241,7 +241,7 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
// Restore the cached CoordinateOperation into the model so
// modelGeoref(mid) returns it without needing the IFC source. Prevents
// sidecar-loaded models from silently losing their georef when the
// .ifc/.rdb sibling is absent or AppSettings.loadDataSource is off.
// .ifc/.rdb sibling is absent.
{
ModelGeoref& gr = model.georef;
gr.has_coordinate_operation = data.has_coordinate_operation != 0;
@@ -276,8 +276,6 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
}
void SceneLoader::startDataSourceLoad(uint32_t mid) {
if (!AppSettings::instance().loadDataSource()) return;
auto it = models_.find(mid);
if (it == models_.end()) return;
@@ -355,13 +353,6 @@ void SceneLoader::onStreamerFinished() {
qint64 ms = it->second.load_timer.elapsed();
emit loadedFromStream(mid, ms);
// Slot(s) above run synchronously (sidecar write uses element_map_,
// not ifcFile()); drop the parsed file now to save memory if the
// user has opted out of keeping a property data source.
if (!AppSettings::instance().loadDataSource()) {
it->second.streamer->setIfcFile(nullptr);
}
}
}
+40 -26
View File
@@ -733,6 +733,26 @@ void ViewportWindow::initGL() {
requestUpdate();
});
// Cull thresholds and HiZ live-toggle: cached cull results assume the
// active values, so any change must invalidate them and request a
// re-render so the new threshold actually takes effect.
auto invalidate_cull = [this]() {
if (!gl_initialized_) return;
have_cached_cull_ = false;
requestUpdate();
};
connect(&AppSettings::instance(), &AppSettings::minPixelRadiusChanged,
this, [invalidate_cull](double){ invalidate_cull(); });
connect(&AppSettings::instance(), &AppSettings::motionMinPixelRadiusChanged,
this, [invalidate_cull](double){ invalidate_cull(); });
connect(&AppSettings::instance(), &AppSettings::lod1PixelThresholdChanged,
this, [invalidate_cull](double){ invalidate_cull(); });
connect(&AppSettings::instance(), &AppSettings::hizEnabledChanged,
this, [this, invalidate_cull](bool){
hiz_vp_valid_ = false; // pyramid no longer trusted
invalidate_cull();
});
gl_initialized_ = true;
flushPendingOperations();
requestUpdate();
@@ -1816,21 +1836,17 @@ void ViewportWindow::fpsIntegrate() {
// (walls, slabs) reliably; finer detail doesn't help much because we're
// sampling the pyramid at the mip level where the AABB's rect is ~2
// texels anyway. Readback cost is ~128 KB/frame ≈ negligible.
// IFC_HIZ_SIZE=<N> overrides the width; height tracks aspect.
// AppSettings::hizResolution() drives the width; height tracks aspect.
// Captured once at first call — changing the setting requires a viewport
// reinitialization to take effect (rebuilding the pyramid mid-session
// would tangle with the framebuffer state machine for little gain).
static int hizBaseWidth() {
static const int w = []{
const char* e = std::getenv("IFC_HIZ_SIZE");
return (e && *e) ? std::max(64, std::atoi(e)) : 256;
}();
static const int w = std::max(64, AppSettings::instance().hizResolution());
return w;
}
static bool hizEnabled() {
static const bool disabled = []{
const char* e = std::getenv("IFC_NO_HIZ");
return e && e[0] == '1';
}();
return !disabled;
return AppSettings::instance().hizEnabled();
}
void ViewportWindow::buildHizPyramid() {
@@ -2314,12 +2330,10 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4],
phase_timer.restart();
// LOD1 switches in when projected sphere radius (in pixels) drops below
// this threshold. Overridable for tuning. Set to 0 to disable LOD1
// entirely (always draw LOD0).
static const float lod1_px_threshold = []{
const char* e = std::getenv("IFC_LOD1_PX");
return (e && *e) ? static_cast<float>(std::atof(e)) : 30.0f;
}();
// this threshold. AppSettings drives it (default 30). Set to 0 in
// settings to disable LOD1 entirely (always draw LOD0).
const float lod1_px_threshold =
static_cast<float>(AppSettings::instance().lod1PixelThreshold());
// Bounding-sphere contribution test: approximate an AABB by its enclosing
// sphere (centre = midpoint, radius = half-diagonal). Project radius to
@@ -2413,9 +2427,13 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4],
// the buffer — a self-reinforcing feedback loop). On static views HiZ
// kicks in after a single frame of lag.
const QMatrix4x4 current_vp = proj_matrix_ * view_matrix_;
// Default on: motion-frame HiZ stays trusted across small VP changes.
// Env var =0 reverts to the strict gate (previous frame's pyramid is
// discarded the moment the view-projection drifts) — useful when
// chasing the missing-geometry class of HiZ correctness bug.
static const bool hiz_force_motion = []{
const char* e = std::getenv("IFC_HIZ_MOTION");
return e && *e && std::atoi(e) != 0;
return !(e && e[0] == '0');
}();
const bool hiz_vp_matches = hiz_vp_valid_
&& (hiz_force_motion || hiz_vp_ == current_vp);
@@ -2640,15 +2658,11 @@ void ViewportWindow::render() {
// culling below.
const float focal_px = 0.5f * static_cast<float>(h) /
std::tan(qDegreesToRadians(0.5f * camera_fov_y_deg_));
static const float base_min_pixel_radius = []{
const char* e = std::getenv("IFC_MIN_PX");
return (e && *e) ? static_cast<float>(std::atof(e)) : 2.0f;
}();
static const float motion_min_pixel_radius = []{
const char* e = std::getenv("IFC_MIN_PX_MOTION");
return (e && *e) ? static_cast<float>(std::atof(e))
: 0.0f; // 0 = disabled (no motion boost)
}();
const float base_min_pixel_radius =
static_cast<float>(AppSettings::instance().minPixelRadius());
// 0 = disabled (motion uses the same threshold as still frames).
const float motion_min_pixel_radius =
static_cast<float>(AppSettings::instance().motionMinPixelRadius());
gl_->glUseProgram(main_program_);
GLint u_vp = gl_->glGetUniformLocation(main_program_, "u_view_projection");
+67
View File
@@ -0,0 +1,67 @@
IfcViewer settings and environment variables
=============================================
User-facing performance and quality settings (min pixel radius, LOD1
threshold, HiZ resolution, etc.) live in the **Settings** dialog and are
persisted via ``QSettings``. This page documents the remaining
environment variables — instrumentation knobs, regression-hunting
toggles, and LOD-build tuning — that are intentionally *not* surfaced
in the GUI because they target developers and benchmark runs.
All variables are read once at first use (most are static-cached
inside the function that consumes them), so set them in the shell
before launching ``IfcViewerFull`` rather than expecting hot-toggle
behaviour.
Diagnostic and benchmark instrumentation
----------------------------------------
These variables expose hooks that are useful when triaging performance
regressions or attributing frame cost. They have no effect on
correctness and ship disabled.
.. csv-table::
:header: "Variable", "Default", "Description"
:widths: 22, 14, 64
"``IFC_HIZ_MOTION``", "on (1)", "Trust the previous frame's HiZ pyramid even when the view-projection has drifted (the default). Set to ``0`` to revert to the strict gate, which discards the pyramid the moment the camera moves — useful when chasing the *missing-geometry* class of HiZ correctness bug, since strict gating reproduces a known-good baseline."
"``IFC_CULL_THREADS``", "on (1)", "Set to ``0`` to disable the multi-threaded culling path. Forces a single-threaded sweep through every model's BVH, which is useful when bisecting a regression suspected to live in the parallel cull."
"``IFC_SKIP_MDI``", "off", "Set to ``1`` to skip ``glMultiDrawElementsIndirect`` calls without changing any other state. Cull, upload, and bind still run; only the actual draw is elided. A large FPS jump means the workload is draw-bound (GPU front-end) rather than upload- or cull-bound."
"``IFC_MAX_SUBDRAWS``", "unlimited", "Truncate the drawcount passed to each MDI to ``N``, preserving the forward/reflected ratio. Lets you isolate per-subdraw command-processor overhead from raw triangle work — sweep ``N`` and watch where the FPS curve flattens."
"``IFC_FPS_HITCH_MS``", "0 (off)", "When non-zero, log a ``[fps-hitch]`` line for any frame that costs more than ``N`` ms. Only active in FPS (first-person) navigation mode. Captures visible objects, sub-draws, and HiZ-rejection counts per hitch so the slowdown can be attributed."
"``IFC_SUBDRAW_DIAG``", "off", "When set (any non-empty value), prints a one-shot histogram of sub-draw composition after the next ``finalizeModel`` — bucket counts of MDIs by sub-draw size, plus instances and triangles in each bucket. Useful for tuning the visible-list packing strategy."
LOD build tuning
----------------
These affect how the LOD1 representation is generated when a sidecar
is *baked*; loading an existing ``.ifcfed`` does not re-read them.
Override only when you're regenerating sidecars and want to inspect or
adjust the trade-off between LOD0 fidelity and LOD1 triangle savings.
.. csv-table::
:header: "Variable", "Default", "Description"
:widths: 22, 14, 64
"``IFC_LOD_ERROR``", "0.05 (clamped to ≥ 0.2)", "``meshopt_simplify`` ``target_error`` parameter — maximum positional error allowed when collapsing edges, normalised to the mesh AABB diagonal. BIM meshes are typically non-manifold and a 0.2 floor still looks fine at sub-4 pixel sizes; smaller values often produce zero collapses on these inputs."
"``IFC_LOD_RATIO``", "meshopt default", "``meshopt_simplify`` ``target_ratio`` parameter — desired fraction of the original index count to retain. Combined with ``target_error`` it forms the simplification budget."
"``IFC_LOD_MIN_SAVINGS``", "0.25", "Minimum fraction of triangles that must be eliminated for the LOD1 result to be accepted. Below this, the LOD1 slot is left empty and LOD0 is always drawn for that mesh — avoids paying upload cost for trivial reductions."
"``IFC_LOD_DEBUG``", "off", "Set to ``1`` to print per-mesh LOD build diagnostics for the first few meshes of each ``buildLodsForSidecar`` call: input/output triangle counts, target error, and the accept/reject decision. Caps printing automatically so it can be left on for full builds without flooding the log."
GUI-promoted settings (no longer env-var driven)
------------------------------------------------
For reference, the following knobs were previously read from
environment variables and are now driven by ``AppSettings`` and the
**Settings** dialog. Their old env-var spellings no longer have any
effect.
.. csv-table::
:header: "Setting", "QSettings key", "Old env var", "Default"
:widths: 28, 32, 22, 18
"Min Pixel Radius", "``viewport/min_pixel_radius``", "``IFC_MIN_PX``", "2.0"
"Motion Min Pixel Radius", "``viewport/motion_min_pixel_radius``", "``IFC_MIN_PX_MOTION``", "10.0"
"LOD1 Pixel Threshold", "``viewport/lod1_pixel_threshold``", "``IFC_LOD1_PX``", "30.0"
"HiZ Occlusion", "``viewport/hiz_enabled``", "``IFC_NO_HIZ`` (inverted)", "on"
"HiZ Resolution", "``viewport/hiz_resolution``", "``IFC_HIZ_SIZE``", "256"
+11 -13
View File
@@ -24,7 +24,6 @@
#include "../../ElementRegistry.h"
#include "../../SessionState.h"
#include "../../../ifcviewer/AppSettings.h"
namespace ifcinterface::modules::properties {
@@ -88,7 +87,17 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
return;
}
if (!AppSettings::instance().loadDataSource()) {
auto entity = registry->findEntity(object_id);
if (entity) {
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
state.property_sets[1].rows[0].value = state.entity.entity_class;
}
} else {
// No live IFC source for this object — typical when a pure-geometry
// .ifcview sidecar was loaded without its .ifc/.rdb sibling. Fall
// back to the basic info cached in the element registry so the
// panel still shows class / name / guid for visible elements.
auto info = registry->findBasicElementInfo(object_id);
if (info && !info->type.isEmpty()) {
state.entity.entity_class = info->type;
@@ -105,17 +114,6 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
if (info && !info->guid.isEmpty()) {
state.attributes[0].value = info->guid;
}
widget_->render(state);
return;
}
auto entity = registry->findEntity(object_id);
if (entity) {
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
state.property_sets[1].rows[0].value = state.entity.entity_class;
}
}
widget_->render(state);
}
-16
View File
@@ -94,18 +94,6 @@ void SettingsDialog::setupUi() {
loading_form->setHorizontalSpacing(16);
loading_form->setVerticalSpacing(10);
load_data_source_checkbox_ = new QCheckBox(loading_body);
load_data_source_checkbox_->setToolTip(
"Keep the .ifc/.rdb open after loading so element properties can be queried. "
"Disable for geometry-only viewing.");
loading_form->addRow("Load Property Data Source", load_data_source_checkbox_);
apply_coordinate_operation_check_ = new QCheckBox(loading_body);
apply_coordinate_operation_check_->setToolTip(
"Apply each model's IfcCoordinateOperation after load so it lands in "
"georeferenced map coordinates.");
loading_form->addRow("Apply Coordinate Operation", apply_coordinate_operation_check_);
void_limit_spin_ = new QSpinBox(loading_body);
void_limit_spin_->setRange(0, 100000);
loading_form->addRow("Void Limit", void_limit_spin_);
@@ -192,8 +180,6 @@ void SettingsDialog::syncFromSettings() {
geometry_library_edit_->setText(AppSettings::instance().geometryLibrary());
show_stats_check_->setChecked(AppSettings::instance().showStats());
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
load_data_source_checkbox_->setChecked(AppSettings::instance().loadDataSource());
apply_coordinate_operation_check_->setChecked(AppSettings::instance().applyCoordinateOperation());
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance());
angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance());
@@ -203,8 +189,6 @@ void SettingsDialog::onAccepted() {
AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text());
AppSettings::instance().setShowStats(show_stats_check_->isChecked());
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
AppSettings::instance().setLoadDataSource(load_data_source_checkbox_->isChecked());
AppSettings::instance().setApplyCoordinateOperation(apply_coordinate_operation_check_->isChecked());
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value());
AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value());
-2
View File
@@ -47,8 +47,6 @@ private:
QLineEdit* geometry_library_edit_ = nullptr;
QCheckBox* show_stats_check_ = nullptr;
QCheckBox* backface_culling_check_ = nullptr;
QCheckBox* load_data_source_checkbox_ = nullptr;
QCheckBox* apply_coordinate_operation_check_ = nullptr;
QSpinBox* void_limit_spin_ = nullptr;
QDoubleSpinBox* deflection_tolerance_spin_ = nullptr;
QDoubleSpinBox* angular_tolerance_spin_ = nullptr;
+5 -16
View File
@@ -84,23 +84,14 @@ ViewportController::ViewportController(ifcinterface::SessionState* session_state
applyModelVisibility(mid);
maybeGuessFederatedFalseOrigin(mid);
});
connect(&AppSettings::instance(),
&AppSettings::applyCoordinateOperationChanged,
this, [this](bool /*enabled*/) {
for (uint32_t mid : session_state_->modelIds()) {
applyCoordinateOperation(mid);
}
});
}
void ViewportController::applyCoordinateOperation(uint32_t mid) {
SceneLoader* loader = session_state_->loader();
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
if (AppSettings::instance().applyCoordinateOperation()) {
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
if (georef->has_coordinate_operation) {
matrix = georef->coordinate_operation_meters;
}
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
if (georef->has_coordinate_operation) {
matrix = georef->coordinate_operation_meters;
}
}
viewport_->setModelCoordinateOperation(mid, matrix);
@@ -118,8 +109,7 @@ void ViewportController::applyModelTransformation(uint32_t mid) {
Eigen::Matrix4d coordinate_operation = Eigen::Matrix4d::Identity();
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
units = georef->units;
if (AppSettings::instance().applyCoordinateOperation() &&
georef->has_coordinate_operation) {
if (georef->has_coordinate_operation) {
coordinate_operation = georef->coordinate_operation_meters;
}
}
@@ -187,8 +177,7 @@ void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) {
if (placement == nullptr || georef == nullptr) return;
federation->setFederatedFalseOrigin(guessFederatedFalseOrigin(
*placement, *georef, federation->config(),
AppSettings::instance().applyCoordinateOperation()));
*placement, *georef, federation->config()));
}
} // namespace ifcinterface::modules::viewport