Commit Graph

10 Commits

Author SHA1 Message Date
Dion Moult 6ca38f8bf6 Derive map units from IFC scale
Use IfcMapConversion.Scale as the source of truth for converting map coordinates to metres, instead of deriving that scale from IfcProjectedCRS.MapUnit. Bump the sidecar version because cached georef matrices and unit scales may differ under the new interpretation.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:19 +10:00
Dion Moult e0a504417c Preserve precise viewer placements
Keep placement transformations in double precision through streaming, sidecar caching, and viewport recomposition so large coordinates can be cancelled before the final GPU float upload.

Generated with the assistance of an AI coding tool.
2026-05-25 16:34:18 +10:00
Dion Moult 8ffdb8f0b9 ifcviewer: cache CoordinateOperation in sidecar (v10 -> v11)
Previously, applyCoordinateOperationToViewport — which pushes both
CoordinateOperation and ModelTransformation — was only called on
paths that required the IFC source to be loaded
(onLoadedFromStream and onDataSourceReady).  Sidecar-only loads
(loadDataSource off, or no .ifc/.rdb sibling) silently lost both
stages.

Cache the per-model georef + unit scales in the sidecar itself so
the IFC source isn't needed to apply them:

  SidecarData gains
    coordinate_operation_meters[16]  // column-major
    project_length_to_meters
    map_unit_to_meters
    has_coordinate_operation

  148 B fixed block written/read between instances and elements.
  SIDECAR_VERSION 10 -> 11; existing sidecars rebuild on next load.

  MainWindow::writeSidecarForModel populates the block from
  loader_->modelGeoref(mid) before writeSidecar.

  SceneLoader::applySidecarData restores it into the model's
  ModelGeoref + sets has_georef = true, so subsequent
  loader_->modelGeoref(mid) calls return the cached data without
  needing the IFC.

  MainWindow::onLoadedFromSidecar now calls
  applyCoordinateOperationToViewport(mid) directly — both
  CoordinateOperation and ModelTransformation land at sidecar-load
  time, no longer waiting on a possibly-never-arriving data source.

Edits to the IFC's IfcMapConversion don't invalidate the cache —
delete the .ifcview manually if the source's georef changes.  This
matches the existing cache-invalidation contract.

Tests: round-trip the new fields through the existing sidecar
fixture; assert SIDECAR_VERSION == 11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:08:45 +10: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 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 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 ababb49ae7 Sidecar v4: persist instanced geometry + metadata
Commit B of the instancing migration.  The sidecar on-disk format is
reintroduced at version 4 with MeshInfo + InstanceCpu sections in place
of v3's flat per-object draw-info array.

After streaming finishes, MainWindow asks the viewport for a post-
finalise snapshot (VBO + EBO are read back from the GPU, meshes and
instances come from the CPU-side arrays) and writes it alongside
PackedElementInfo + the string table.  On a subsequent load,
readSidecar rehydrates the whole struct and ViewportWindow::
applyCachedModel uploads VBO/EBO/SSBO in a single step, bypassing the
iterator entirely.

Staleness check is still by source file size.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 07a5c59359 GPU instancing: streamer, viewport, shaders rewritten
Commit A of the instancing migration (Phase 3a).  The streamer now runs
the iterator with use-world-coords=false and dedupes by the geometry's
representation id, emitting a MeshChunk once per unique geometry and an
InstanceChunk per placement.  The viewport keeps geometry in local
coordinates (28 B/vertex, down from 32) and applies the per-instance
transform in the vertex shader via an std430 SSBO indexed by
gl_InstanceID + a per-draw uniform offset.  After streaming finishes
finalizeModel() stable-sorts instances by mesh_id, assigns each mesh a
contiguous range, and uploads the SSBO; render then issues one
glDrawElementsInstancedBaseVertex per mesh.

BvhAccel is reshaped to operate on a generic BvhItem (world AABB +
model_id) so it can drive instance-level culling, but the path is not
wired in yet -- every instance is drawn every frame in this commit.
Progressive-during-streaming rendering is likewise disabled: a model
appears when its SSBO is uploaded, not incrementally.  Sidecar cache
is stubbed (reads miss, writes are no-ops); the v4 on-disk format with
MeshInfo + InstanceGpu sections lands in Commit B.

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