Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
907fd03
Bump util from v0.8.0 branch
Moult Apr 11, 2026
b137953
Local hacks to compile and monkey patch issues in the Python world
Moult Apr 11, 2026
92372f4
Dump of hello world ifc viewer code
Moult Apr 11, 2026
3c2a503
More hacks to compile
Moult Apr 11, 2026
fe498ed
Update ifcviewer to compile with datamodel refactor
Moult Apr 11, 2026
750cc89
Plan out performance strategy
Moult Apr 11, 2026
8bd1dee
Per-object frustum culling with glMultiDrawElements
Moult Apr 11, 2026
6a00184
Add performance stats overlay in status bar
Moult Apr 11, 2026
2ffd2da
Multi-model project support with sequential loading
Moult Apr 11, 2026
49b70e8
Update README for multi-model support and frustum culling
Moult Apr 11, 2026
4d5256b
BVH frustum culling, sidecar cache, per-model buffers, progressive up…
Moult Apr 11, 2026
2d9f3fb
Add profiling for VRAM, FPS ratios, and instancing analysis
Moult Apr 12, 2026
d8362e2
Leaf-batched BVH draw commands
Moult Apr 12, 2026
1097fa3
GPU instancing: streamer, viewport, shaders rewritten
Moult Apr 12, 2026
ea640e2
Sidecar v4: persist instanced geometry + metadata
Moult Apr 12, 2026
f4b3e6c
BVH frustum culling over instances
Moult Apr 12, 2026
e2d5130
Progressive rendering during streaming
Moult Apr 12, 2026
99ca61b
Collapse per-mesh draws into glMultiDrawElementsIndirect
Moult Apr 12, 2026
0473348
Two-sided lighting, rename misleading draw-count stat
Moult Apr 12, 2026
d1cdec2
Enable reorient-shells in geometry iterator
Moult Apr 12, 2026
4729a09
Backface culling with reflection-aware two-pass MDI
Moult Apr 12, 2026
f32f471
Rewrite README for instancing pipeline and refocus Phase 3
Moult Apr 12, 2026
8360cd3
Pivot Phase 3: diagnose as draw-bound, not upload-bound
Moult Apr 12, 2026
09c1eef
Phase 3A: screen-space contribution culling
Moult Apr 12, 2026
95b1b97
README: mark Phase 3A done with measured numbers
Moult Apr 13, 2026
3fe183e
Phase 3B: per-instance LOD via meshoptimizer simplifySloppy
Moult Apr 13, 2026
91c8e46
Phase 3C: Hierarchical-Z occlusion culling (CPU-side v1)
Moult Apr 13, 2026
cc680df
Cull: read AABBs from compact bvh_items in the hot path
Moult Apr 14, 2026
a173992
Cull: skip cullAndUploadVisible + HiZ on still frames
Moult Apr 14, 2026
bbe644e
ifcviewer: event-driven rendering, idle scenes cost zero CPU
Moult Apr 14, 2026
91198c9
ifcviewer: quantize VBO to 16 B/vertex (sidecar v6)
Moult Apr 14, 2026
036864c
ifcviewer: README — document event-driven rendering and VBO quantization
Moult Apr 14, 2026
f243f80
ifcviewer: parallel per-model CPU cull
Moult Apr 15, 2026
c0e99a1
ifcviewer: README — document parallel per-model cull (Phase 3D)
Moult Apr 15, 2026
6b496d8
ifcviewer: disable HiZ cull when camera has moved
Moult Apr 15, 2026
1caf449
ifcviewer: fix pick-pass cull corruption and cached-model ID collisions
Moult Apr 15, 2026
196f984
ifcviewer: README — document HiZ disabled during camera motion
Moult Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh
set -e

mkdir -p build && cd build

cmake ../cmake \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DPython_EXECUTABLE=/home/dion/Projects/env/bin/python3.11 \
-DPython_INCLUDE_DIR=/usr/include/python3.11 \
-DBUILD_IFCPYTHON=ON \
-DBUILD_IFCGEOM=ON \
-DBUILD_CONVERT=ON \
-DBUILD_GEOMSERVER=OFF \
-DBUILD_EXAMPLES=OFF \
-DWITH_OPENCASCADE=ON \
-DWITH_CGAL=ON \
-DWITH_MANIFOLD=ON \
-DHDF5_SUPPORT=OFF \
-DGLTF_SUPPORT=ON \
-DIFCXML_SUPPORT=OFF \
-DCOLLADA_SUPPORT=OFF \
-DSCHEMA_VERSIONS="2x3;4;4x3_add2" \
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
-DOCC_LIBRARY_DIR=/usr/lib64/opencascade

ninja

cp ifcwrap/_ifcopenshell_wrapper*.so ifcwrap/ifcopenshell_wrapper.py \
../src/ifcopenshell-python/ifcopenshell/
7 changes: 6 additions & 1 deletion cmake/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ option(BUILD_EXAMPLES "Build example applications." ON)
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON)
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
option(BUILD_IFCVIEWER "Build IfcViewer, a high-performance IFC viewer" OFF) # Requires Qt6 + OpenGL 4.5
option(BUILD_PACKAGE "" OFF)

option(WITH_OPENCASCADE "Enable geometry interpretation using Open CASCADE" ON)
Expand Down Expand Up @@ -332,7 +333,8 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
# set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
endif()

if(USE_MMAP)
Expand Down Expand Up @@ -712,6 +714,9 @@ if(BUILD_IFCGEOM)

install(TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom)
endif(BUILD_IFCGEOM)
if(BUILD_IFCVIEWER)
add_subdirectory(../src/ifcviewer ifcviewer)
endif()

# Cmake uninstall target
if(NOT TARGET uninstall)
Expand Down
317 changes: 317 additions & 0 deletions findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# Build fix: remove `boost_system` from CMake components

`Boost.System` became header-only in Boost 1.69. Boost 1.90.0 no longer ships a compiled library or CMake config for it, so `find_package(Boost REQUIRED COMPONENTS system ...)` fails.

## Fix

`cmake/CMakeLists.txt`:

```diff
- set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
+ set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
```

The headers are still available; no linking is needed.

# Build fix: add `template` keyword for dependent template member calls

Calling a template member function through a dependent expression (e.g. `storage->has_attribute_value<T>(...)` where `storage`'s type depends on a template parameter) requires the `template` keyword to disambiguate from a less-than comparison.

## Error

```
src/ifcparse/IfcParse.cpp:1856:67: error: expected primary-expression before '>' token
1856 | if (storage->has_attribute_value<express::Base>(attr_index)) {
| ^
```

Six identical errors at lines 1856, 1865, 1896, 1905, 1934, 1943.

## Fix

`src/ifcparse/IfcParse.cpp`:

```diff
-storage->has_attribute_value<express::Base>(attr_index)
+storage->template has_attribute_value<express::Base>(attr_index)

-storage->has_attribute_value<Blank>(attr_index)
+storage->template has_attribute_value<Blank>(attr_index)
```

Applied at all six call sites in `in_memory_file_storage::read_from_stream`.

# Linker fix: missing explicit template instantiations for `InstanceStreamer`

`InstanceStreamer` is a class template with methods defined in `IfcParse.cpp`, not the header. Without explicit instantiations, the linker can't find the symbols when the SWIG wrapper loads.

## Error

```
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPS3_PNS_7IfcFileE
(IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcFile*))
```

## Fix

Cannot use `template class InstanceStreamer<...>` because some constructors have `static_assert` guards that reject certain reader types. Instead, instantiate each member function individually per reader type, only including the constructors valid for that type.

`src/ifcparse/IfcParse.cpp` (after the last `InstanceStreamer` method definition):

```cpp
// FullBufferImpl
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(const std::string&, bool, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcParse::IfcFile*);
// ... plus ensure_header, initialize_header, hasSemicolon, semicolonCount,
// pushPage, bypassTypes, readInstance

// PushedSequentialImpl — same pattern, different valid constructors

// MMapFileReader (ifdef USE_MMAP) — same pattern
```

# Linker fix: `FullBufferImpl` missing buffer constructor

SWIG's `stream_from_string` calls `InstanceStreamer<FileReader<FullBufferImpl>>(void*, int, IfcFile*)`, but the `(void*, int)` constructor previously hit a `static_assert` for `FullBufferImpl` — it only allowed `PushedSequentialImpl`.

## Error

```
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPviPNS_7IfcFileE
(InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcFile*))
```

## Fix

Three changes to make `FullBufferImpl` support buffer-based and default construction:

`src/ifcparse/FileReader.h` — add buffer constructor to `FullBufferImpl`:

```diff
class IFC_PARSE_API FullBufferImpl {
public:
explicit FullBufferImpl(const std::string& fn);
+ FullBufferImpl(void* data, size_t length);
```

`src/ifcparse/FileReader.h` — add `FileReader(void*, size_t)` forwarding constructor:

```diff
+ FileReader(void* data, size_t length)
+ : cursor_(0) {
+ if constexpr (std::is_same_v<Impl, FullBufferImpl>) {
+ impl_ = std::make_shared<Impl>(data, length);
+ } else {
+ static_assert(...);
+ }
+ }
```

`src/ifcparse/FileReader.cpp` — implement the constructor:

```cpp
FullBufferImpl::FullBufferImpl(void* data, size_t length)
: buf_(static_cast<char*>(data), static_cast<char*>(data) + length)
, size_(length) {
}
```

`src/ifcparse/IfcParse.cpp` — extend the two `InstanceStreamer` constructors to accept `FullBufferImpl`:

```diff
// InstanceStreamer(IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(nullptr, (size_t)0);

// InstanceStreamer(void*, int, IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(data, (size_t)length);
```

# Runtime fix: segfault in `parse_context::push()` due to vector reallocation

`parse_context_pool` stores nodes in a `std::vector<parse_context>`. During parsing, `load()` takes a `parse_context&` parameter and calls `context.push()`, which calls `pool_->make()`. If the pool's vector reallocates (via `emplace_back`), all existing references into the vector — including the `context` reference held by the caller — become dangling. Subsequent access through the dangling reference causes a segfault.

Triggered by larger IFC files (e.g. `ISSUE_159_kleine_Wohnung_R22.ifc`, 9.5 MB) that cause enough pool growth to trigger reallocation.

## Error

```
Thread 1 received signal SIGSEGV, Segmentation fault.
0x... in IfcParse::parse_context::push()
#1 in_memory_file_storage::load(...) // context& is dangling after reallocation
#2 in_memory_file_storage::load(...) // parent call
#3 InstanceStreamer::readInstance()
```

## Fix

`src/ifcparse/storage.h` — change the pool container from `std::vector` to `std::deque`, which does not invalidate references on `push_back`/`emplace_back`:

```diff
+#include <deque>

struct parse_context_pool {
- std::vector<parse_context> nodes_;
+ std::deque<parse_context> nodes_;
```

# Runtime fix: `express::Base` comparison operators throw on null/expired instances

`express::Base::operator<` and `operator==` called `data()`, which throws `std::runtime_error("Trying to access deleted instance reference")` when the internal `weak_ptr` is expired. A default-constructed `express::Base` (the value-type equivalent of a null pointer) always has an expired `weak_ptr`.

## Why this model triggers it

The bug requires two conditions to coincide:

1. A representation is shared by **more than one product** (via `IfcRepresentationMap` / `IfcMappedItem`).
2. At least one of those products has **no material association**, so `get_single_material_association()` returns `express::Base{}` (the null equivalent).

In `advanced_model.ifc`, Body representations like `#449` (Body/Brep) have a single `IfcRepresentationMap` (`#453`) with 13 `IfcMappedItem` usages, meaning 13 products share the geometry. Some of those products (e.g. `IfcFlowTerminal` instances) have no `IfcRelAssociatesMaterial`, so `get_single_material_association` returns `express::Base{}`.

Smaller or simpler models don't hit this because either:
- Every representation maps to only 1 product → `reuse_ok_` short-circuits at `products.size() == 1` before reaching the material check.
- Every product has a material association → no null `express::Base` is ever inserted into the set.

## Exact call sequence

```
Iterator::initialize()
try {
mapping::get_representations(reps, filters_)
addRepresentationsFromDefaultContexts(representations)
→ collects reps from subcontexts in order:
Axis (#115): 143 reps
Body (#117): 7550 reps
FootPrint (#119): 12 reps

for (auto representation : representations):

── Axis reps (indices 0–142) ──────────────────────────
products_represented_by(rep, rmap)
→ OfProductRepresentation: 1 product each
filter_products(products, filters) → 1 product
reuse_ok_(ifcproducts)
→ products.size() == 1 → return true ← SHORT-CIRCUIT, no material check
representation_mapped_to(rep) → null (no MappedItem)
→ task created. 143 tasks accumulated.

── First Body rep #449 (Body/Brep) ────────────────────
products_represented_by(#449, rmap)
→ OfProductRepresentation: empty
→ RepresentationMap: 1 map (#453)
→ MapUsage: 13 MappedItems → traces through to 13 IfcProducts
filter_products(products, filters) → 13 products
reuse_ok_(ifcproducts) ← CRASH HERE
→ products.size() == 1? NO (13 products)
→ for each product:
find_openings(product) → OK
get_single_material_association(product)
→ some products have no IfcRelAssociatesMaterial
→ returns express::Base{} (expired weak_ptr)
associated_single_materials.insert(result)
→ std::set::insert calls operator<
→ operator< calls data()
→ data() calls data_.lock() → expired → THROWS
"Trying to access deleted instance reference"

} catch (const std::exception& e) {
Logger::Error(e) ← exception caught here, get_representations aborted
}

→ reps contains only the 143 Axis tasks created before the throw
→ all 143 Axis reps have Curve2D geometry → map(representation) returns null
→ no valid elements produced → initialize() returns false
```

In the old pointer-based code, `reuse_ok_` used `std::set<const IfcUtil::IfcBaseEntity*>` and `get_single_material_association` returned `nullptr`. Inserting `nullptr` into a `std::set<T*>` is a plain pointer comparison — no dereference, no throw. The refactoring to `std::set<express::Base>` changed the comparison from pointer comparison to `express::Base::operator<`, which unconditionally dereferences through `data()`.

## Error

```
[Error] Trying to access deleted instance reference
[Notice] Created 143 tasks for 143 products ← only Axis reps; all Body reps lost
initialize() returned: False
```

## Fix

`src/ifcparse/express.h` — use `weak_ptr::lock().get()` instead of `data()` so that expired pointers compare as `nullptr` (matching old raw-pointer semantics):

```diff
bool operator<(const Base& other) const {
- return data() < other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() < b.get();
}

bool operator==(const Base& other) const {
- return data() == other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() == b.get();
}
```

# Runtime fix: `entity_instance` missing `get_inverse` due to SWIG `%rename` collision

Accessing inverse attributes (e.g. `element.IsDecomposedBy`) on any entity raises `AttributeError: entity instance of type 'IFC2X3.IfcProject' has no attribute 'get_inverse'`.

## Why

`entity_instance_mixin.__getattr__` (line 106 of `entity_instance.py`) calls `self.get_inverse(name)` when it detects an inverse attribute. Since the mixin inherits into the SWIG-generated `entity_instance` class (via the `object = custom_base` hack in `IfcParseWrapper.i:936`), `self.get_inverse` must resolve to a method on the SWIG class.

However, `IfcParseWrapper.i:70` has a global rename:

```
%rename("get_inverses_by_declaration") get_inverse;
```

This was intended for `ifcopenshell::file::get_inverse` (which takes an entity + declaration and returns instances by reference), but SWIG `%rename` is global — it also renames the `%extend express::Base` method `get_inverse(const std::string& a)` at line 551. So the Python-side `entity_instance` class exposes the method as `get_inverses_by_declaration`, not `get_inverse`.

The old code (`v0.8.0`) didn't hit this because `__getattr__` called `self.wrapped_data.get_inverse(name)` on an inner `ifcopenshell_wrapper.entity_instance` object — but in that old layout, the inner object was constructed differently and the rename didn't apply the same way (or the method had a different path). In the new mixin approach, `self` **is** the SWIG object, so the rename is directly visible.

## Fix

`src/ifcwrap/IfcParseWrapper.i` — override the global rename specifically for `express::Base::get_inverse`, restoring the original name on entity instances:

```diff
+%rename("get_inverse") express::Base::get_inverse;
%rename("get_inverses_by_declaration") get_inverse;
```

Add this line **before** the global rename (or anywhere before the `%extend express::Base` block). This scoped rename takes precedence for `express::Base`, so:
- `entity_instance.get_inverse(name)` works as the mixin expects
- `file.get_inverses_by_declaration(...)` keeps its intended name

## Python-side workaround

`entity_instance.py:106` — call the method by its SWIG-renamed name:

```diff
- vs = self.get_inverse(name)
+ vs = self.get_inverses_by_declaration(name)
```

# Runtime fix: `entity_instance` class no longer importable from `entity_instance` module

The class rename from `entity_instance` to `entity_instance_mixin` broke external code that does `from ifcopenshell.entity_instance import entity_instance`.

## Error

```
ImportError: cannot import name 'entity_instance' from 'ifcopenshell.entity_instance'
```

Triggered at import time via `ifcopenshell.util.pset` (and likely other modules).

## Fix

`src/ifcopenshell-python/ifcopenshell/entity_instance.py` — add a backwards-compatible alias at the bottom of the module:

```python
entity_instance = entity_instance_mixin
```
2 changes: 1 addition & 1 deletion src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances& inst

// at this point, next == end and prev == end-1
#if defined SCHEMA_HAS_IfcDistanceExpression
double last_distance = (*prev)->DistanceAlong() * length_unit_;
double last_distance = (*prev).DistanceAlong() * length_unit_;
#else
double last_distance = (double) prev->DistanceAlong().as<IfcSchema::IfcLengthMeasure>() * length_unit_;
#endif
Expand Down
3 changes: 3 additions & 0 deletions src/ifcopenshell-python/ifcopenshell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
from .file import rocksdb_lazy_instance
from . import guid
from .ifcopenshell_wrapper import entity_instance
from .entity_instance import _patch_swig_comparisons
_patch_swig_comparisons()
del _patch_swig_comparisons
from .sql import sqlite, sqlite_entity

# explicitly specify available imported symbols
Expand Down
Loading
Loading