wgpu backend: selection visualisation + global object_id rebase

Closes the loop on stage 4 (pick): clicking an object now highlights it
on screen. Plus the prerequisite plumbing for selection to behave
correctly across multi-sidecar loads.

Pieces:

  1. WgpuSelectionState (new header)
     CPU-side multi-set + active-id, mirroring the GL Selection.h shape
     but pure stdlib (no Qt deps) so it can move into ifcviewer-core
     later without dragging Qt across. clear/replace/add/remove/toggle
     APIs + a fillFlagsArray helper that packs (selected, active) into
     a u32 bitmap indexed by object_id.

  2. selection_flags storage buffer + frame_bgl bump to 2 entries
     Indexed by object_id, bit 0 = selected, bit 1 = active. Lives in
     the frame bind group (group=0 binding=1) because object_ids are
     globally unique — making it model-scoped would be the wrong cut.
     ensureSelectionFlagsBuffer grows geometrically (64 → 128 → … u32)
     as new models push next_object_id_ up, rebuilds the frame bind
     group when it does.

  3. Global object_id rebase in applyCachedModel
     Each sidecar's local ids start from 1 and collide across files;
     pick was previously ambiguous on multi-model loads. We now add
     next_object_id_ as a base offset, rewrite InstanceCpu.object_id
     (CPU mirror stays consistent) + InstanceGpu.object_id (what pick
     reads back), and bump next_object_id_ by the model's max + 1.

  4. WGSL main fragment reads sel_flags
     Vertex shader passes inst.object_id through to fragment as
     @interpolate(flat). Fragment reads sel_flags[object_id], mixes
     (0.2, 0.6, 1.0) at 0.45 for in-selection and (0.4, 0.8, 1.0) at
     0.40 on top for active. Same constants as the GL main shader.

  5. Mouse → selection
     LMB-click-without-drag pick result feeds the selection:
       no modifier → replace
       Shift      → add
       Ctrl       → remove (active migrates to another id in the set)
       miss + no modifier → clear
     uploadSelectionFlagsIfDirty repacks + writes the GPU bitmap at
     the top of the next render(); no upload on still frames.

Pick pipeline is unchanged — it already outputs the per-instance
object_id, and that's what the selection storage indexes.

Visibility + clip planes are pending follow-ups in stage 5 (mostly
small, share the same buffer-lifecycle pattern). Edge silhouette
(stage 9 partial), --screenshot diff harness (stage 10 partial),
ifcviewer-core extract (stage 12), and web chunking (stage 13) all
still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-27 20:25:05 +10:00
parent 1bbcd10bc0
commit 54fa7d8379
3 changed files with 257 additions and 20 deletions
+23 -1
View File
@@ -34,6 +34,7 @@
#include "SidecarCache.h"
#include "WgpuModelGpuData.h"
#include "WgpuSelectionState.h"
// Stage-2 wgpu viewport: opens a native QWindow, brings up a wgpu instance/
// adapter/device, configures a surface against the platform-native window
@@ -126,6 +127,14 @@ private:
void releaseEdgeResources();
bool buildPickPipeline();
// Make sure selection_flags_buffer_ is large enough to address every
// object_id in next_object_id_. Recreates (and rebuilds frame_bind_group_)
// if it grew. Safe to call every frame; idempotent when already sized.
void ensureSelectionFlagsBuffer();
// Repack the CPU selection into bit-flags and wgpuQueueWriteBuffer to
// the GPU. Called from render() when selection_.dirty().
void uploadSelectionFlagsIfDirty();
void ensurePickAttachments(int w, int h);
void releasePickResources();
// Synchronous pick: encodes a one-shot R32UInt render of the current
@@ -214,6 +223,15 @@ private:
WGPUBuffer frame_uniform_buffer_ = nullptr;
WGPUBindGroup frame_bind_group_ = nullptr;
// Selection flags storage buffer at group=0 binding=1. u32-per-object_id,
// bit 0 = selected, bit 1 = active. Sized to next_object_id_ rounded up;
// grows when a load pushes past the current capacity. Bound in the
// frame bind group because object_ids are globally unique across models.
WGPUBuffer selection_flags_buffer_ = nullptr;
uint32_t selection_flags_capacity_ = 0; // number of u32 entries
WgpuSelectionState selection_;
std::vector<uint32_t> selection_flags_scratch_;
// Depth attachment (4× MSAA), recreated on surface resize.
WGPUTexture depth_texture_ = nullptr;
WGPUTextureView depth_view_ = nullptr;
@@ -334,7 +352,11 @@ private:
// Per-model state, keyed by viewport-assigned model_id.
std::unordered_map<uint32_t, WgpuModelGpuData> models_gpu_;
uint32_t next_model_id_ = 1;
uint32_t next_model_id_ = 1;
// Globally-unique object_id allocator. Each applyCachedModel rebases
// the sidecar's local object_ids by base_object_id_so_far so picks
// are unambiguous across models. Selection flags index this range.
uint32_t next_object_id_ = 1;
// Sidecar paths queued before init completes.
std::deque<QString> pending_sidecars_;