Commit Graph

30 Commits

Author SHA1 Message Date
Dion Moult bb17cfbc40 Rename Bonsai Viewer build option
Replace the old IFC viewer build switch with BUILD_BONSAIVIEWER in CMake, the Linux workflows, and the nix build script.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Thomas Krijnen 554c7174e3 Backspace everything regarding HDF5 2026-05-08 16:20:26 +02:00
Dion Moult 633c613da2 ifcviewer: drop unused SidecarHeader reserved field, bump v8 -> v9
The reserved uint32_t was always written as 0 and never inspected on
read.  Removing it shrinks the header from 16 to 12 bytes; the version
bump makes pre-existing sidecars fail the version check cleanly rather
than misreading by 4 bytes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:40:00 +10:00
Dion Moult 4f929e90a7 ifcviewer: key sidecar on path stem, drop staleness check
Previously readSidecar/writeSidecar were keyed on (path, file_size) with
staleness rejected at read time.  Switch to pure path-stem keying: foo.ifc
and foo.ifcdb/ both resolve to foo.ifcview, so the same cache serves either
source format.  Staleness is user-managed (delete the sidecar to force a
rebuild), which also lets sidecars be copied or moved independently of the
source.

v8 header drops the source_file_size field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 5161b0a3f8 ifcviewer: remove meshopt_simplify path, keep only simplifySloppy
Edge-collapse decimation (meshopt_simplify) returns BIM meshes unchanged
due to per-triangle vertex duplication and non-manifold topology. The
sloppy voxel-clustering decimator is faster, needs no shadow index
welding, and produces good results at the sub-30px LOD1 threshold.
Remove the non-sloppy branch, shadow buffer, IFC_LOD_SLOPPY and
IFC_LOD_LOCK_BORDER env vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 3015f758ba ifcviewer: shrink vertex format from 16 to 12 bytes (oct i8x2 normals)
Replace i16x2 octahedral normals with i8x2, filling the 2-byte padding
after position and saving 4 bytes per vertex. int8 gives ~1.4 deg
worst-case angular error — invisible for BIM geometry which is
overwhelmingly axis-aligned. 25% VBO reduction; sidecar files shrink
~15% overall (5.4 GB -> 4.6 GB on a 111-model test scene). Bumps
sidecar format to v7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult ed6e8d831e ifcviewer: benchmark CLI, settle recull fix, and Phase 3G documentation
Add --camera tx,ty,tz,dist,yaw,pitch and --benchmark N CLI args for
reproducible performance measurement.  The benchmark orbits the camera
(0.5°/frame yaw) for N frames after a 5-frame warmup, prints
avg/median/p1/p99 frame times, then exits.  Press C during interactive
use to print the current camera as a --camera argument.

Fix settle recull to fire after ANY camera motion (not just when
IFC_MIN_PX_MOTION is set), ensuring HiZ artifacts from motion frames
are always cleared when the camera stops.

Document Phase 3G (motion-adaptive culling + HiZ during motion) in
README with benchmark results from 1.06M-instance scene:
  - Baseline:                    16.3 fps
  - IFC_MIN_PX_MOTION=10:       26.5 fps (1.6x)
  - IFC_HIZ_MOTION=1:           46.6 fps (2.9x)
  - Both combined:              51.0 fps (3.1x)
  - + GPU_CULL:                 52.0 fps (3.2x, negligible gain)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 930678e3d2 ifcviewer: motion-adaptive contribution culling + sub-draw diagnostics
During camera motion, use a larger pixel-radius threshold (IFC_MIN_PX_MOTION)
to aggressively cull small objects, dramatically reducing sub_draws and
improving orbit fps (e.g. 29→67 fps on 1M-instance scene).  When the camera
stops, automatically re-cull at the base threshold to restore full detail.

Key behaviors:
- IFC_MIN_PX_MOTION=N sets the motion threshold (0 = disabled)
- Settle recull fires on the first still frame after motion
- HiZ pyramid invalidated on settle (stale from sparse motion frame)
- GPU cull results skipped on settle (dispatched at motion threshold)
- requestUpdate() ensures the settle frame actually runs

Also adds IFC_SUBDRAW_DIAG=1 diagnostic for sub-draw composition analysis
and documents Phase 3E/3F experiment results in README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 9aae8f0329 Revert "ifcviewer: GPU cull drives rendering under IFC_GPU_CULL=1"
This reverts commit 4fe32b54105ca2c5c00290603db17164837211e1.
2026-04-23 21:32:24 +10:00
Dion Moult 643a2e1c1f Revert "ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)"
This reverts commit 77cac3ec170b622db6977829f66b62603266a047.
2026-04-23 21:32:24 +10:00
Dion Moult 3c5c8e44cb Revert "ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)"
This reverts commit 9a7a48944f4b62f9ca431149139eb846229f6114.
2026-04-23 21:32:24 +10:00
Dion Moult fc89ffeb19 Revert "ifcviewer: MDI compaction via glMultiDrawElementsIndirectCount"
This reverts commit d5b7b87ba17c90008cf0673c838ce8431ad85e36.
2026-04-23 21:32:24 +10:00
Dion Moult 4bedb40d8a ifcviewer: MDI compaction via glMultiDrawElementsIndirectCount
Pack compute shader compacts non-empty indirect commands into
contiguous fwd/rev ranges, eliminating ~690k empty sub-draws that
dominated command-processor overhead.  GL 4.6 entrypoint loaded via
getProcAddress with ARB fallback; graceful degradation to uncompacted
MDI when unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult a34c36d22e ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)
Two-phase compute-cull dispatch when IFC_GPU_CULL=1:

  Phase 1  frustum + contribution + LOD, no HiZ  → survivors
  Depth    render survivors depth-only into half-viewport FBO
  Build    GPU compute max-reduce depth → R32F mip pyramid
  Phase 2  same cull + HiZ test                  → final survivors
  Color    render final survivors

The compact shader's new hizOccluded() projects 8 AABB corners to
screen space, picks the mip level where the covered rect fits in ≤2×2
texels, and rejects when the AABB's near-depth exceeds the pyramid's
max depth.

New GPU resources (per-window):
  hiz_gpu_fbo_ / hiz_gpu_depth_tex_  — depth-only FBO at half viewport
  hiz_gpu_pyramid_tex_                — R32F mipmapped pyramid
  hiz_gpu_copy_prog_                  — compute: depth → pyramid L0
  hiz_gpu_reduce_prog_                — compute: max-reduce L(n-1)→L(n)
  hiz_gpu_depth_prog_                 — vertex + trivial fragment

On a dense 18-model BIM dataset:
  survivors:  140k → 65k  (HiZ rejects ~50%)
  triangles:  22M  → 13M
  gpu_cull:   0.06ms → 22.5ms  (depth pre-pass CP overhead)

The depth pre-pass suffers the same empty-sub-draws CP overhead as the
color pass (690k commands, most with instanceCount=0).  Once MDI
compaction lands, both passes will be fast.  For now, net FPS is flat
(savings on color ≈ cost of depth pre-pass).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult e5ed7b53d4 ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)
The compact shader now computes per-instance pixel radius and routes
survivors to LOD1 buckets when the projected sphere falls below the
LOD1 threshold (default 30 px, same as CPU path, tunable via
IFC_LOD1_PX).

Layout expanded from 2 to 4 buckets per mesh:
  [0..M)   fwd_lod0   [M..2M)   fwd_lod1
  [2M..3M) rev_lod0   [3M..4M)  rev_lod1

Two MDIs per model: CCW for [0..2M), CW for [2M..4M).  Per-mesh
has_lod1 flags live in a new gpu_mesh_flags_ssbo (binding 4).

Contribution cull refactored: the compact shader now computes
pixelRadius() once and uses it for both the min_pixel_radius rejection
and LOD routing, matching the CPU path's logic.

Visible-buffer worst case is 2 × total_instances (each LOD bucket
reserves the full fwd/rev capacity per mesh, since LOD selection is
dynamic).

Tri count drops ~60% on the test dataset (53M → 22M) thanks to LOD1
decimated meshes.  FPS recovers from 16 to 36 despite 690k sub_draws
(4M layout).  MDI compaction remains the final perf fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0b122ae1f5 ifcviewer: GPU cull drives rendering under IFC_GPU_CULL=1
Promote the compute cull from a validation shader to the actual draw
driver.  With the gate on, the CPU cull fan-out is skipped and MDI
consumes gpu_indirect_buffer / gpu_visible_ssbo directly.

- uploadGpuCullStaticBuffers() pre-fills per-mesh DrawElementsIndirect
  commands and a mesh_base prefix sum so the compact shader can scatter
  survivors into a fixed per-mesh range.  Instance count for each
  command is zeroed by a tiny reset dispatch, then the compact shader
  atomically writes survivors and increments instanceCount.
- Draw loop branches on the gate: single CCW MDI with all mesh
  commands.  Fwd/rev winding split, LOD selection, and HiZ are still
  CPU-path-only; reflected instances render with wrong winding under
  this gate (step 3b).
- Once-per-second readback of each model's indirect buffer populates
  the survivor / visible-object / visible-triangle stats so the
  [frame] line reflects what the GPU actually drew.

Known regression: sub_draws is the full mesh count per model (~172k on
the test dataset) vs the handful of non-empty commands the CPU path
produces.  Command-processor overhead from zero-instance sub-draws is
what drives the FPS drop, not the cull itself (0.05 ms).  Compacting
non-empty commands requires glMultiDrawElementsIndirectCount, a GL 4.6
entrypoint not exposed by Qt's QOpenGLFunctions_4_5_Core; deferring to
3a-followup so we don't bolt a getProcAddress loader into the renderer
mid-restructure.

IFC_GPU_CULL is off by default, so this does not affect normal runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0a752e09eb ifcviewer: README — document HiZ disabled during camera motion
The 'Known caveats' bullet still described the old 1-frame-stale
behavior.  Since 6b496d802 the cull compares hiz_vp_ to the current VP
and drops HiZ rejection whenever they differ, so HiZ only helps on
still frames — orbiting gets no benefit.  Call out the tradeoff and
the planned same-frame-depth-pre-pass fix slated for Phase 3E.
2026-04-23 21:32:24 +10:00
Dion Moult 03662d2016 ifcviewer: fix pick-pass cull corruption and cached-model ID collisions
Two stability bugs:

1. Clicking an object left the scene with wrong shading until the camera
   moved.  The pick pass re-culls every model with its own parameters
   (min_pixel_radius=0, no HiZ) and overwrites each model's visible_ssbo
   and indirect buffer.  The next render() saw an unchanged camera,
   skipped the cull via the have_cached_cull_ shortcut, and drew the
   stale pick-pass buffers.  Fix: invalidate have_cached_cull_ at the
   end of pickObjectAt().

2. Loading two sidecar-cached models made the second model's picked
   properties resolve to the first model's elements.  Sidecars store raw
   object_id / model_id values from the session that wrote them, and
   both files start at object_id=1, so element_map_ entries collided.
   Fix: on load, rebase every PackedElementInfo and InstanceCpu by
   (next_object_id_ - min_id_in_sidecar) and overwrite model_id with
   the freshly-assigned handle before the elements hit element_map_.

Also document both in the README — the pick-pass note under 3A
contribution culling, the sidecar rebase under the sidecar format
section.
2026-04-23 21:32:24 +10:00
Dion Moult 0c9d3ea6d7 ifcviewer: README — document parallel per-model cull (Phase 3D)
Add the parallel cull bullet to the feature list, a Phase 3D section
explaining the fan-out / scratch-ownership design + measured 4x
speedup, and renumber the planned GPU compute cull to Phase 3E so it
can cite 3D as the CPU algorithm being ported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1ec273f508 ifcviewer: README — document event-driven rendering and VBO quantization
Add the event-driven rendering bullet (zero idle cost, in-render frame
timing) and roadmap entries for VBO quantization and event-driven
rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 574bcfa6c5 ifcviewer: quantize VBO to 16 B/vertex (sidecar v6)
Position now u16x3 normalized against each mesh's local AABB; normal
oct-encoded to i16x2; RGBA8 colour unchanged. Per-mesh dequant basis
lives in a new MeshGpu SSBO at binding 2; both main and pick shaders
mix() against it before applying the instance transform.

Drops VBO and sidecar size by ~43 % (28 -> 16 B/vert), which matters
mostly for warm-load downloads of precomputed sidecars and steady-state
VRAM. LodBuilder dequantizes positions into a scratch buffer before
calling meshopt, since meshoptimizer needs float positions.

Also fixes a streaming-time crash in cullAndUploadVisible: bvh_items
was only populated at finalize, but the linear fallback indexes it
during streaming. Mirror BvhItem appends in uploadInstanceChunk so the
hot path stays valid before the BVH is built.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 8596d53a4a Phase 3C: Hierarchical-Z occlusion culling (CPU-side v1)
After the main draw, blit the MSAA default-framebuffer depth to a
single-sample 256×128 depth texture, read it back, and build a CPU
max-reduced mip pyramid.  Next frame's cullAndUploadVisible projects
each BVH node / instance AABB through the previous frame's VP and
compares the AABB's nearest depth against the pyramid's deepest value
at the matching mip level; strictly-beyond AABBs are rejected.

Conservative direction (aabb_near > hiz_max) — never wrongly rejects a
visible instance, so no flicker.  BVH subtree-level test lets a single
8-corner projection reject up to a leaf's worth of instances.

Tuning knobs: IFC_NO_HIZ=1 disables; IFC_HIZ_SIZE overrides base width.
New stats counter hiz_rej shows rejects/frame.

Measured: big win on interior views (GPU-bound), roughly zero net
effect on exterior overviews (CPU-bound on cull traversal, so the
saved GPU work is masked).  Tried a 3-deep PBO ring for async readback
and reverted — the extra frame of staleness produced visible flicker
on fast orbit, and the synchronous readback wasn't actually a measured
bottleneck at 256×128.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult c78e16eafb Phase 3B: per-instance LOD via meshoptimizer simplifySloppy
Decimate each unique mesh once at sidecar-build time and swap to the
reduced index slice per-instance per-frame when projected sphere radius
drops below IFC_LOD1_PX (default 30).  Same VBO, same SSBO, just a
different firstIndex/count in the indirect command.

Extends MeshInfo (48→56 B) with lod1_ebo_byte_offset + lod1_index_count
and bumps the sidecar to v5.  buildLods() runs inside
onStreamingFinished, appends decimated indices to sd.indices,
applyLodExtension pushes the EBO suffix to the live GPU state, and the
sidecar is written with LOD1 baked in.

simplifySloppy (voxel clustering) is used instead of the default
edge-collapse meshopt_simplify because BIM brep output is per-triangle-
unwelded and non-manifold after welding — simplify returned the input
unchanged for every mesh tested.  Sloppy ignores topology.  Knobs
(IFC_LOD_SLOPPY, IFC_LOD_ERROR, IFC_LOD_RATIO, IFC_LOD_MIN_SAVINGS,
IFC_LOD_LOCK_BORDER, IFC_LOD_DEBUG) are available for A/B tuning.

Result on the 128M-tri 10-model test scene (GTX 1650, 2px contribution
cull): 20.2 → 43.2 fps, 40M → 14M visible triangles, no change in
object count.  LOD build adds 100–600 ms per model on first open,
cached thereafter.

README Phase 3B section is now a full writeup of pipeline, selection,
decimator-choice rationale, env vars, and measured numbers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 68fea7bd45 README: mark Phase 3A done with measured numbers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d3c21d7a81 Pivot Phase 3: diagnose as draw-bound, not upload-bound
Earlier probes pointed at per-frame glNamedBufferSubData uploads as the
bottleneck (60 fps when those two calls were commented out).  That was a
false reading — zeroing the uploads also emptied the indirect buffer, so
MDI drew nothing.  "No upload" and "no draw" were indistinguishable.

Two new diagnostic env vars in render() isolate the real costs:

  IFC_SKIP_MDI=1       keep cull + upload + binds, skip only the MDI
                       draws.  Gives 62 fps with everything else running,
                       confirming the non-draw path fits in ~16 ms.
  IFC_MAX_SUBDRAWS=N   cap each MDI's drawcount.  67k -> 30k sub-draws
                       saves 0 ms, confirming sub-draw count itself is
                       not the bottleneck; the long tail of sub-draws
                       carries ~no triangles.

On a GTX 1650 with 128 M triangles in view, nvidia-smi sits at 95 %
GPU util and FPS scales with triangle work, not sub-draw count.  The
card is simply rasterising at ~850 M tri/s.  No CPU-side or upload
trick recovers it.

Revised Phase 3 is therefore shedding triangles, not bytes:
  3A screen-space contribution culling (next)
  3B LOD
  3C HiZ occlusion
  3D GPU-side compute culling

README Phase 3 section rewritten around the diagnosis, including the
false lead, so future work doesn't re-tread the upload path.  The
aborted staging+resident ring-buffer implementation was reverted (the
uncommitted working tree is gone — pure glNamedBufferSubData retained
for the visible + indirect buffers, which we now know is fine).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult cd77c557e9 Rewrite README for instancing pipeline and refocus Phase 3
The previous README described a pre-instancing world (32-byte world-
coord vertices with per-vertex object_id, ObjectDrawInfo structs, EBO
reordering after BVH build, and a Phase 3 plan built around moving
draw submission to the GPU).  Most of that is either gone or already
solved:

  - Vertices are now 28 B local-coord; per-instance transforms live
    in an SSBO read through a visible-index SSBO and gl_BaseInstanceARB.
  - ObjectDrawInfo is replaced by MeshInfo + InstanceCpu + InstanceGpu.
  - No EBO reorder on BVH build — the BVH is over instance AABBs and
    the mesh/EBO layout is orthogonal.
  - Draw-call submission is already one glMultiDrawElementsIndirect
    per model; the old Phase 3 goal is met.

New content worth keeping:

  - GPU instancing section documents the mesh/instance/visible/indirect
    buffer contract the whole renderer hangs off of.
  - Reflection-aware two-pass draw is documented (det<0 placements,
    forward/reverse slice split, glFrontFace toggle).
  - reorient-shells and backface culling are called out as correctness
    + perf levers with their tradeoffs.
  - Phase 3 is rewritten around the actual bottleneck surfaced by
    profiling: per-frame glNamedBufferSubData stalls on the visible
    and indirect buffers.  Includes the diagnostic methodology (empty-
    screen jump to 60 fps, window/MSAA invariance, upload-comment-out
    experiment) so future-me remembers why this is the next step.
  - 3A (persistent mapped ring buffers, near-term) and 3B (GPU-side
    compute cull, longer-term) split out with scope estimates.
  - Roadmap updated: instancing / MDI / reflections / reorient-shells
    / backface cull all ticked; 3A surfaced as the next open item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1dace18d26 BVH frustum culling, sidecar cache, per-model buffers, progressive upload
Phase 2 performance: BVH acceleration with median-split build, per-model
trees, and EBO re-sorting for GPU cache coherence. Raw binary .ifcview
sidecar stores full geometry + BVH for instant subsequent loads (skip
tessellation entirely).

Per-model GPU buffers (VAO/VBO/EBO per model) eliminate cross-model buffer
copies on growth. Sidecar reads happen on a background thread. Bulk GPU
uploads are progressive (48 MB/frame chunks) so the viewport stays
interactive while multi-GB models stream in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 5b4c1089cf Update README for multi-model support and frustum culling
Reflect current architecture: per-model streamers, glMultiDrawElements
with frustum culling, 32-byte vertex format with color, multiselect
file picker, settings/stats files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d33055bb72 Plan out performance strategy 2026-04-23 21:32:24 +10:00
Dion Moult 06eca938d7 Dump of hello world ifc viewer code 2026-04-23 21:32:24 +10:00