docs: rewrite stale GL-era docs (env-vars + viewport_architecture)

Two long-stale docs that described the deleted OpenGL backend are
replaced with current-state rewrites under `src/bonsaiviewer/docs/`
and wired into the toctree. The originals are removed.

## env-vars.rst (replaces src/ifcviewer/settings.rst)

The orphan `src/ifcviewer/settings.rst` was written for the OpenGL
backend (`IFC_*` prefix, MDI-specific knobs) and was never wired into
any Sphinx toctree — it sat as a one-off file in the C++ source tree,
undiscoverable from a normal docs build.

* **Dead — dropped entirely.** `IFC_SKIP_MDI`, `IFC_MAX_SUBDRAWS`,
  `IFC_SUBDRAW_DIAG` were GL-only `glMultiDrawElementsIndirect`
  instrumentation. wgpu has no MDI. `IFC_FPS_HITCH_MS` no longer
  exists in source.
* **Renamed.** `IFC_HIZ_MOTION` → `WGPU_HIZ_MOTION`,
  `IFC_CULL_THREADS` → `WGPU_CULL_THREADS`.
* **New, previously undocumented.** Ten `WGPU_*` vars added during
  the port + bring-up (WGPU_HIZ, WGPU_HIZ_TRACE, WGPU_MIN_PX,
  WGPU_MIN_PX_MOTION, WGPU_FLY_DEBUG, WGPU_NAV_PRESET,
  WGPU_PRESENT_MODE, WGPU_STREAM_DEBUG, WGPU_STREAM_DEEP_DEBUG,
  WGPU_STREAM_EVICT_LOG). Descriptions written from each variable's
  use-site so wording matches actual behaviour.
* **LOD-build section kept verbatim.** IFC_LOD_ERROR, IFC_LOD_RATIO,
  IFC_LOD_MIN_SAVINGS, IFC_LOD_DEBUG — sidecar-bake knobs,
  backend-agnostic.
* **GUI-promoted "old IFC_* graveyard" section dropped.** The file
  is an env-var reference, not a record of historical spellings.

## viewport_architecture.rst (replaces src/ifcviewer/README.md)

The 994-line `src/ifcviewer/README.md` was an archive of the GL-era
phase-by-phase perf narrative. ~95% of it described deleted code:
OpenGL 4.5 Core, `glMultiDrawElementsIndirect`, VAO/VBO/EBO,
`GL_ARB_shader_draw_parameters`, BVH-per-model, sidecar v5/v7/v9
(current is v13), the now-non-existent `./IfcViewer` binary, Phase
3F "static batching next" plans superseded by the chunk-pool
architecture, Phase 3E "GPU compute culling removed" since re-added
as task #17 pending. Salvaging the ~50 lines of still-correct
content would have left a Frankenstein doc internally contradicting
itself.

Replaced with a focused architecture page covering current reality:
consumer split (BonsaiViewer shell vs IfcViewerMinimal standalone),
stack (wgpu-native v29, Qt6, IfcOpenShell, IfcUtil, Eigen3,
meshoptimizer), five core ideas (unique-mesh instancing, quantized
12 B vertex, chunked streaming on a probed VRAM pool, sidecar v13
fast path, event-driven rendering), per-frame pipeline (cull →
upload → streaming → opaque pass → transparent pass → edge → overlay
→ present), federation + false-origin compose, file map limited to
files that actually exist in `src/ifcviewer/` today, build/run via
`build_viewer.sh`, cross-refs to env-vars.rst, debug-output.rst,
and connectors/.

## Toctree

`src/bonsaiviewer/docs/index.rst` gains `env-vars` and
`viewport_architecture` entries alongside the existing
`connectors/index` and `debug-output`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-04 15:57:40 +10:00
parent ce7d2fa329
commit 749476d1a7
3 changed files with 334 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
Environment variables and developer settings
============================================
User-facing performance and quality settings (min pixel radius, LOD1
threshold, HiZ resolution, …) 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 ``BonsaiViewer`` rather than expecting hot-toggle
behaviour.
Renderer (wgpu) knobs
---------------------
These steer the wgpu backend's renderer / cull / streaming subsystems.
They have no effect on geometric correctness and ship disabled.
.. csv-table::
:header: "Variable", "Default", "Description"
:widths: 26, 14, 60
"``WGPU_HIZ``", "off (0)", "Set to ``1`` to enable HiZ occlusion culling. Disabled by default since task #58 surfaced HiZ false-rejection bugs on certain camera angles; will return to on-by-default once the strict-vs-loose gating is settled."
"``WGPU_HIZ_MOTION``", "off (0)", "Trust the previous frame's HiZ pyramid even when the view-projection has drifted (matches the old GL backend's behaviour). Default ``0`` is strict: any VP delta invalidates the pyramid for this frame, so transparent windows and other view-dependent surfaces aren't culled against stale depth. Set to ``1`` when chasing perf to re-enable the loose path."
"``WGPU_HIZ_TRACE``", "off", "Set to ``1`` to arm a one-shot per-frame log when HiZ is about to reject instances. Dumps the pyramid's bottom rows + per-rejection details. Useful for diagnosing the *missing-geometry* class of HiZ correctness bug — task #58."
"``WGPU_MIN_PX``", "GUI value", "Override the ``viewport/min_pixel_radius`` setting from the shell. Stationary contribution-cull threshold in pixels: instances whose sphere projection falls below this are dropped from the visible list. Useful for sweeping in benchmarks without flipping the GUI value back and forth."
"``WGPU_MIN_PX_MOTION``", "GUI value", "Same as above for ``viewport/motion_min_pixel_radius`` — the (typically larger) threshold applied while the camera is moving."
"``WGPU_CULL_THREADS``", "on", "Set to ``0`` to disable the per-model ``std::async`` dispatch. Forces every model's cull pass to run sequentially on the main thread — used to measure the parallel-cull speedup and to bisect regressions suspected to live in the worker join."
"``WGPU_PRESENT_MODE``", "(auto)", "Override the surface present mode. Accepts ``fifo``, ``fifo_relaxed``, ``mailbox``, or ``immediate``. Default picks the first of Mailbox → Immediate → FifoRelaxed → Fifo that the surface actually advertises. Useful for fly-mode input-latency triage — see :doc:`debug-output` for the full rationale."
"``WGPU_FLY_DEBUG``", "off (0)", "Set to ``1`` to print a per-frame ``[fly] dt=X.XXms render_gap=Y.YYms`` line while fly mode is active. Diagnoses pacing irregularities — see the fly-mode latency notes in :doc:`debug-output`."
"``WGPU_NAV_PRESET``", "``blender``", "Mouse-navigation preset. ``blender`` (MMB orbit, Shift+MMB pan, scroll dolly), ``rhino``, or ``revit``. Selection always stays on LMB. Mirrors the GL backend's preset model — see ``AppSettings::NavPreset``."
"``WGPU_STREAM_DEBUG``", "off (0)", "Set to ``1`` to enable per-frame ``[stream-debug]`` logging. Reports residency state, pool occupancy, eviction decisions, and per-chunk priority scores. Used to triage streaming residency thrash on big federations."
"``WGPU_STREAM_DEEP_DEBUG``", "off", "When set (any value), every 120th frame in interactive mode dumps the priority-projection AABBs of all currently *missing* visible chunks. Lets the loader's chunk-priority math be inspected without breaking out of normal navigation. Pairs with ``WGPU_STREAM_DEBUG``."
"``WGPU_STREAM_EVICT_LOG``", "off", "When set (any value), each eviction prints the candidate chunk + the chunk it displaced plus their priority scores. Useful when ``driveStreamingLoads`` appears to thrash but the per-frame totals look stable."
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: 26, 14, 60
"``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."
+3
View File
@@ -39,6 +39,9 @@ files and viewer-oriented formats such as ``.ifcview`` and ``.rdbview``.
:maxdepth: 2
connectors/index
debug-output
env-vars
viewport_architecture
Need more help? Join the `live chat <https://osarch.org/chat/>`__ or `community
forums <https://community.osarch.org>`__. Something strange happening? Please
@@ -0,0 +1,277 @@
Viewport architecture
=====================
This page documents the IFC viewer renderer that lives at
``src/ifcviewer/`` and underpins both BonsaiViewer and the standalone
``IfcViewerMinimal`` testbed.
Consumers
---------
``src/ifcviewer/`` is a static library (``IfcViewer``) — it does not
ship as an executable on its own. Two consumers link against it:
* **BonsaiViewer** (``src/bonsaiviewer/``) — full project shell with
side panels, federation tree, settings dialog, connector picker,
ribbon. The end-user product.
* **IfcViewerMinimal** (``src/ifcviewer-minimal/``) — single-file
testbed that opens an IFC, a ``.ifcview`` sidecar, or a federation
``.ifcfed`` and shows just the viewport. Used for screenshot
regression tests, benchmarks, and isolating renderer-only issues
from the Bonsai Viewer shell.
Stack
-----
* **wgpu-native v29** for rendering, fetched as a pre-built binary in
``src/ifcviewer/CMakeLists.txt``. WebGPU API on top of Vulkan
(Linux/Windows) or Metal (macOS, via the ``MetalSurface_mac``
Cocoa bridge).
* **Qt6** for windowing (a raw ``QWindow``, not ``QOpenGLWidget``)
and the surface integration with the OS. Qt is also the source of
``QSettings`` for persisted user preferences (``AppSettings``),
the event loop driving render scheduling, and the timer/elapsed
primitives used in instrumentation.
* **IfcOpenShell C++ libs** (IfcParse / IfcGeom) for IFC parsing and
geometry generation, plus **IfcUtil** for the schema-agnostic
helpers (``Unit``, ``Geolocation``, ``Placement``).
* **Eigen3** for 4×4 matrices and small linear algebra.
* **meshoptimizer** at sidecar-build time only — decimates each
unique mesh into an LOD1 slice. Not pulled at runtime.
Five core ideas
---------------
The renderer is built around five decisions worth knowing about
before reading the code:
**1. Unique-mesh GPU instancing.** IFC scenes are dominated by
repeated geometry (identical doors, windows, studs, pipes, …). The
``IfcGeom::Iterator`` surfaces representation identity, so each
unique mesh is uploaded once into a per-model vertex buffer slice
and every placement becomes a tiny ``InstanceCpu`` record
(transform + ``object_id`` + optional colour override). On a 1 M-
placement BIM scene this collapses tens of millions of duplicate
vertices into a few hundred MB of unique meshes.
**2. Quantized 12-byte vertex format.** Each vertex is exactly 12
bytes:
.. code-block:: text
offset 0 pos 3 × uint16 normalised → mix(mesh.aabb_min, mesh.aabb_max, t)
offset 6 normal 2 × int8 normalised → [-1,1]; octahedral-decoded
offset 8 color 4 × uint8 normalised → [0,1]; alpha is real (see "Transparency")
The dequantisation basis is per-mesh, uploaded once in a
``MeshGpu`` SSBO at binding 2; the vertex shader reads it via the
mesh index that comes through the visible-draws table. ``int8``
normals carry ~1.4° worst-case angular error, invisible for the
overwhelmingly axis-aligned faces (walls, floors, slabs) BIM models
produce.
**3. Streaming chunked storage backed by a probed VRAM pool.** Per-
model state isn't loaded all at once. The sidecar's vertex/index
data is split into spatially-coherent chunks (Morton-sorted at bake
time, packed greedily); ``ViewportWindow`` keeps each chunk's bytes
resident in a single GPU buffer pool whose size is determined at
startup by ``wgpuDevicePushErrorScope(OutOfMemory)`` probing. As
the camera moves, chunks page in and out via the
``StreamingThread`` background worker; the render thread never
issues a blocking disk read.
**4. Sidecar cache as the fast path.** Loading an IFC the first
time runs the ``IfcGeom::Iterator`` (expensive); the result is
serialised to a ``.ifcview`` file (``SIDECAR_VERSION = 13``) next
to the source. Subsequent opens go straight from disk to GPU
buffers — no iteration, no geometry-engine cost. The sidecar
format embeds chunked vertex sections with a byte-range table of
contents so the streaming thread can fetch any chunk by ``pread``
without scanning the file.
**5. Event-driven rendering.** No render timer. Frames are
scheduled only via ``QWindow::requestUpdate()`` when something
actually changes — camera motion, streaming chunk arrival, hover,
selection, settings edits. When the camera is idle, the cull pass
short-circuits and the main thread blocks in the Qt event loop;
the viewer costs zero CPU/GPU on a static scene.
Per-frame pipeline
------------------
Each call to ``ViewportWindow::render()`` runs the same six phases:
.. code-block:: text
render():
1. fpsIntegrate() ← advance fly-mode camera by wall-clock dt
2. drainHizReadbacks() ← absorb HiZ readback completions
3. uploadSelectionFlagsIfDirty()
4. cullModelCpuCompute() ← parallel per-model frustum+contrib+HiZ+LOD
cullModelCpuUpload() ← writeBuffer visible-draws + prefix-sums
5. driveStreamingLoads() ← enqueue chunk fetches, apply completions
6. encode passes:
opaque main (depthWriteEnabled=True, no blend)
transparent main (depthWriteEnabled=True, SrcAlpha/InvSrcAlpha blend)
edge silhouette (samples depth buffer)
overlay (HUD, labels, gizmos, measurements, marquee)
wgpuSurfacePresent()
The two-pass alpha split (#6) routes each visible instance into
either the opaque half or the transparent half of the per-chunk
``visible_draws_scratch`` based on a per-mesh ``has_alpha`` flag +
the instance's ``color_override_rgba8`` alpha byte. The transparent
pass uses the same buffers, just with ``firstVertex`` offset to the
opaque half's end. ``Alt+X`` forces every instance through the
transparent pass via the ``xray_alpha_cap`` frame uniform, giving a
free X-ray mode.
Cull
~~~~
CPU-side, runs parallel per model via ``std::async``. The cascade
per instance is:
1. **Chunk-level frustum cull** — each chunk's AABB is tested first;
off-frustum chunks skip every instance inside them in one shot.
2. **Per-instance frustum cull** — for chunks that pass.
3. **Contribution cull** — instances whose sphere projects below
``viewport/min_pixel_radius`` (stationary) or
``viewport/motion_min_pixel_radius`` (during motion) get dropped.
4. **HiZ occlusion** — projected AABB tested against the previous
frame's depth pyramid, when the VP matches. The pyramid is
captured at end-of-frame, downsampled, read back to CPU, and
queried with conservative all-fine-texels-agree semantics. Off
by default; ``WGPU_HIZ=1`` re-enables.
5. **LOD selection** — instances with sub-``viewport/lod1_pixel_threshold``
projected size and an available LOD1 slice route through the
LOD1 index range. Same vertex buffer, different ``firstIndex``.
Survivors are appended to the chunk's ``visible_draws_scratch`` and
cumulative ``prefix_sums_scratch`` — the vertex shader uses a
binary search on prefix_sums to translate ``vertex_index`` into a
``(draw_index, vertex_in_draw)`` pair.
Streaming
~~~~~~~~~
The buffer pool (``BufferPool``) is a free-list sub-allocator over
one or more wgpu buffers, sized at startup by probing for
``WGPUErrorType_OutOfMemory`` on a series of growing allocations.
``StreamingThread`` is one worker thread that owns disk I/O:
``driveStreamingLoads()`` posts requests for the highest-priority
non-resident chunks and drains completions. The pool's eviction
policy is "evict the lowest-priority resident chunk whose priority
is below ``candidate_priority / EVICT_PRIORITY_RATIO``" — strict
enough to prevent thrash, lax enough that a single panning frame
doesn't refuse the move.
Priorities come from a per-chunk screen-space score computed during
cull (chunk AABB projected to a screen-rect area). Spatial sort at
bake time means screen-adjacent chunks are byte-adjacent on disk,
so the typical fetch is a single ``pread`` range, not scatter-
gather.
Overlay / picking / section cuts
--------------------------------
* ``OverlayRenderer`` runs after the main passes. Bundles the HUD
text, world-anchored labels (measurement readouts), the
marquee-select rect, the corner axis gizmo, the orbit pivot
indicator, and section-plane outline rendering. Has its own
pipelines with the usual ``SrcAlpha/OneMinusSrcAlpha`` blend.
* **Picking** is a second render pass with a dedicated pipeline
writing object IDs into an ``R32UInt`` framebuffer. Clicking
triggers a ``wgpuQueueOnSubmittedWorkDone`` + ``mapAsync``
readback of one pixel. No CPU-side raycasting.
* **Section cuts** (the ``K`` tool) push up to six clipping planes
into the frame uniform; ``is_section_clipped()`` in WGSL discards
fragments on the positive side. The plane gizmo and visible
cross-section outline come from OverlayRenderer.
Federation
----------
``Federation`` (in ``IfcViewer``, not Bonsai) is the multi-model
data model. Each model in a federation has a fed_id, optional
group membership, an explicit ``ModelTransformation``, a
``CoordinateOperation`` (from the IFC's IfcMapConversion), and a
visibility flag. The viewport composes a single
``transform_meters`` per instance as:
.. code-block:: text
federated_false_origin · model_transformation · coord_op · placement
``federated_false_origin`` is shared across all models in the
session and cancels the bulk of the big surveyor coordinates BIM
files carry, so float32 vertex math stays well within precision.
The first-added model's first geometry point auto-seeds the false
origin via ``ViewportView::guessFederatedFalseOriginFromFirstModel``
when the add-model command arms the guess; see that function for
why the arm/consume mechanism exists (it replaced a stack-
overflowing slot-emit-in-slot recursion).
Files map
---------
Render core (the wgpu side):
* ``ViewportWindow.{h,cpp}`` — the main render window. Owns the
wgpu surface, device, queue, all pipelines, the per-model
``ModelGpuData``, the cull pass, the frame uniforms. ~7500 lines;
the bulk of the renderer.
* ``ModelGpuData.h`` — per-model GPU state (chunks, instances,
meshes, pool slices, alpha flags, scratch buffers used by cull).
* ``InstancedGeometry.h`` — wire structs shared by the streamer
and the viewport (``MeshInfo``, ``InstanceCpu``, ``InstanceGpu``,
``MeshChunk``, ``InstanceChunk``, vertex layout constants).
* ``VertexQuantization.h`` — pack/unpack helpers for the 12-byte
vertex format.
* ``BufferPool.{h,cpp}`` — VRAM sub-allocator with multi-sub-buffer
growth.
* ``StreamingThread.{h,cpp}`` — background ``pread`` worker.
* ``StreamingLoader.{h,cpp}`` — chunk-priority planner driving the
thread; owns the eviction policy and the residency map.
* ``OverlayRenderer.{h,cpp}`` — HUD / labels / gizmos / section-cut
outline / marquee. Separate pipelines + WGSL shader strings.
* ``MetalSurface_mac.{h,mm}`` — Cocoa bridge for the CAMetalLayer
surface attach on macOS. Compiled only on Apple via the
``OBJCXX`` language CMake enables.
* ``SelectionState.h``, ``VisibilityState.h`` — pure CPU state
machines for the selection set and the hidden-objects set;
header-only; covered by ``ifcviewer/tests/test_selection.cpp``
and ``test_visibility.cpp``.
* ``AreaMeasurement.{h,cpp}``, ``LengthMeasurement.{h,cpp}`` — the
measurement tools (triangle-area accumulation; world-space
polyline distance).
IFC ingestion and on-disk format:
* ``GeometryStreamer.{h,cpp}`` — wraps ``IfcGeom::Iterator`` on a
background thread; emits ``MeshChunk`` (unique geometry) and
``InstanceChunk`` (one per placement) to ``SceneLoader``.
* ``SceneLoader.{h,cpp}`` — orchestrates load. Owns the per-model
``ifcopenshell::file`` for property lookup; detects sidecar
presence; serialises queue of pending loads.
* ``SidecarCache.{h,cpp}`` — binary read/write of ``.ifcview``
files. Magic ``IFVW``, version 13 (see the per-version comment
block at the top of the header for the schema evolution).
* ``SidecarBuilder.{h,cpp}`` — live-load sidecar writer. Accumulates
the data as the streamer emits it, writes the sidecar after
``finalizeModel``.
* ``LodBuilder.{h,cpp}`` — meshoptimizer wrapper that builds each
mesh's LOD1 index slice at sidecar-bake time. Gated behind
``WITH_MESH_OPTIMIZER``; otherwise the LOD1 slot is left empty
and the renderer always uses LOD0.
Federation + settings:
* ``Federation.{h,cpp}`` — multi-model session: groups, model
transforms, coordinate operations, the federated false origin.
Persisted as ``.ifcfed``. Covered by
``ifcviewer/tests/test_federation.cpp``.
* ``AppSettings.{h,cpp}````QSettings``-backed preferences:
``viewport/min_pixel_radius``, ``viewport/lod1_pixel_threshold``,
``viewport/hiz_enabled``, ``viewport/nav_preset``, etc. See
:doc:`env-vars` for the full key list.