From 749476d1a7f75f7fd64ba8564e3973595390c13b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 4 Jun 2026 15:57:40 +1000 Subject: [PATCH] docs: rewrite stale GL-era docs (env-vars + viewport_architecture) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/bonsaiviewer/docs/env-vars.rst | 54 ++++ src/bonsaiviewer/docs/index.rst | 3 + .../docs/viewport_architecture.rst | 277 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 src/bonsaiviewer/docs/env-vars.rst create mode 100644 src/bonsaiviewer/docs/viewport_architecture.rst diff --git a/src/bonsaiviewer/docs/env-vars.rst b/src/bonsaiviewer/docs/env-vars.rst new file mode 100644 index 0000000000..2be242a305 --- /dev/null +++ b/src/bonsaiviewer/docs/env-vars.rst @@ -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." diff --git a/src/bonsaiviewer/docs/index.rst b/src/bonsaiviewer/docs/index.rst index 65d88160f1..b12c22776c 100644 --- a/src/bonsaiviewer/docs/index.rst +++ b/src/bonsaiviewer/docs/index.rst @@ -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 `__ or `community forums `__. Something strange happening? Please diff --git a/src/bonsaiviewer/docs/viewport_architecture.rst b/src/bonsaiviewer/docs/viewport_architecture.rst new file mode 100644 index 0000000000..af5cd17f59 --- /dev/null +++ b/src/bonsaiviewer/docs/viewport_architecture.rst @@ -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.