mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
d368ee449d3cfef7bce3dde8ab070b27110d8468
22 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a1693259b8 |
wgpu backend: BVH cull (opt-in via --bvh, default off)
Stage 15 implementation lands but doesn't pay off as default-on. On a 562k-instance / 18-model scene with a centred camera, the BVH walk adds ~10 ms of cull cost without rejecting enough subtrees to compensate — every interior node's AABB straddles the frustum, so descents go all the way to leaves anyway. Linear scan beats it by that 10 ms. GL's BVH works better mainly because they do full cull (frustum + HiZ + contribution) at every node — their per-test cost is lower (likely SIMD-vectorised) and they get more subtree rejections. My current impl does frustum-only at interior nodes (HiZ there cost more than it saved on the smaller dataset). For now, gate the whole BVH walk behind --bvh, default off. The infrastructure (BvhAccel build at applyCachedModel, walk in cull, release) stays in place so it's a one-flag toggle to measure either side. Real default-on requires further tuning — see updated task #15. Measured on 562k-instance scene: --bvh on → 25.9ms total (cull 25.4ms) --bvh off → 15.4ms total (cull 14.5ms) ← default For comparison, GL on the same scene + camera: GL → 18.2ms total (cull 8.5ms wall, multi-threaded BVH) Net: wgpu beats GL by ~3ms total despite slower cull, because the GPU side (no edge-pass cost, async HiZ readback, lean main pipeline) gives back more than the cull deficit. Also added task #17 (GPU compute-shader cull) as the asymptotic answer — both backends hit CPU cull as the ceiling on ≥500k scenes; moving it to a compute shader drops it to sub-ms regardless. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7dc13eb104 |
wgpu backend: chunk vertex storage to fit browser limits + settle frame
Two pieces: 1. Per-chunk vertex storage (stage 13) WebGPU mandates maxStorageBufferBindingSize ≥ 128 MB. Real BIM models routinely exceed that (one of yours is 139 MB vertex). Without chunking, every browser load would fail with "exceeds max_storage_buffer_binding_size". Strategy: each model's vertex data is split into ≤ 128 MB chunks at applyCachedModel time. Each chunk gets its own vertex_storage buffer, visible_draws / prefix_sums buffers, per_chunk_uniform, and bind group. Index buffer, instance storage, and mesh storage stay single-per-model (they fit well under the cap on every scene we've seen). Mesh-to-chunk assignment is bake-time-deterministic (walks meshes in order, opens a new chunk when adding the next would overflow). Cull buckets visible instances by their mesh's chunk; render issues one drawcall per non-empty chunk per model. WGSL is unchanged — the binary-search vertex pulling works identically per chunk because base_vertex is now CHUNK-LOCAL (the chunk's bind group binds its own vertex_storage). Single code path: chunking is ALWAYS on at 128 MB regardless of target. Cost on desktop is a handful of extra drawcalls per frame (1 per non-empty chunk; typical models = 1-3 chunks). Negligible. A mesh whose vertex range is itself > 128 MB can't fit in any chunk and would need splitting — typical IFC meshes are nowhere near that (hundreds of verts), and applyCachedModel warns loudly if one ever appears. --web-limits CLI flag requests the WebGPU mandatory floor limits (128 MB max storage binding, 256 MB max buffer) instead of the adapter's actual max. Used to verify chunking actually fits through browser constraints — turns "trust me, web will work" into a hard test. The 139 MB scene loads cleanly with --web-limits. 2. Settle frame after motion (bug fix) Reported regression: after orbiting, sub-pixel instances dropped by motion-mode contribution culling stayed missing after the camera stopped. Event-driven rendering means no frame is scheduled after mouse-up, so the cull never re-ran at the still threshold. Fix: track last_cull_was_motion_. If this frame used the motion threshold, requestUpdate() after present to schedule one settle frame. Next frame: camera_moved = false → still threshold → small instances reappear. Matches GL's last_cull_was_motion_ behaviour. Verified pixel-identical on basic.ifc; loads the user's dense scene successfully under --web-limits (chunks=2 on the 139 MB model, chunks=1 on the others). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4dfe27e251 |
wgpu backend: per-element visibility + H/Shift+H/I hotkeys
Closes the interactive selection loop. After this commit you can: - LMB-click an object → highlight (selection) - Press H → hide all selected - Press Shift+H → show all (clear hidden set) - Press I → isolate selected (hide everything else) WgpuVisibilityState (new header) is a plain unordered_set<uint32_t> of hidden object_ids — mirrors src/ifcviewer/Visibility.h's shape but stays Qt-free for the ifcviewer-core extract later. cullModelCpuCompute consults visibility_.isHidden(inst.object_id) before the frustum test — hidden instances cost nothing on every axis (no draw, no depth contribution, no pick hit). The CPU vector is read concurrently by the parallel cull workers, which is safe because mutations only happen between renders (handlers requestUpdate after mutating; render reads). Hiding deselects (matches GL behaviour: H clears the now-invisible selection rather than leaving phantom selected-but-invisible ids). Stage 5's last piece — clip planes — is deferred. Adding the uniform array + WGSL discard is mechanical, but the section-tool UI that drives them isn't ported yet (minimal viewer has no way to place a clip plane), so it'd ship as empty plumbing. Will land alongside the section-tool port. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
54fa7d8379 |
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>
|
||
|
|
1bbcd10bc0 |
wgpu backend: pick pass with sync R32UInt readback
Stage 4 of the wgpu port. LMB-click-without-drag now resolves the
object_id under the cursor by running a dedicated pick render and
copying back the single texel at the click position.
- Pick pipeline reuses the existing pipeline_layout_ (same bindings
as main: frame uniform at group=0, per-model storages at group=1).
Different vs / fs entry points (vs_pick / fs_pick) in the main
WGSL module — the vertex pulling logic is duplicated for now but
the bind group layout match means no pipeline_layout rebuild and
pickObjectAt can reuse the current frame's already-uploaded
visible_draws + per-model bind groups.
- Pick FBO: surface-sized R32UInt color attachment + Depth32Float
depth, both single-sample (no MSAA — pick needs exact texel
access). CopySrc on the color so we can copyTextureToBuffer the
1×1 click region. Recreated on surface resize.
- pickObjectAt: encodes a one-shot pick pass + a single texel copy
into a 256-byte staging buffer, submits, mapAsync, sync-spins
processEvents until ready. Synchronous wait is fine here — pick
runs on click, not per-frame, so a sub-ms stall is invisible.
- Mouse integration: existing LMB drag-orbit preserved. A 3-pixel
threshold promotes drag (set nav_dragged_); release without
dragging triggers pickObjectAt at the release coords (logical
Qt → physical pixels via devicePixelRatio). object_id is logged;
selection state to consume the id arrives with stage 5.
Object_id 0 means miss (clear value); the pick attachment is cleared
to 0 before each pass and the fragment writes the instance's
object_id, so any non-zero result is a real hit on a drawn instance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f4243038bd |
wgpu backend: edge silhouette post-process (ported GL renderEdgePass)
First piece of stage 9 — the dark outlines BonsaiViewer / the GL backend
draw at depth discontinuities. Ported the GL renderEdgePass algorithm
verbatim, including the three things my earlier attempt missed:
1. Linearise depth to view-space metres before the Laplacian. Raw
[0,1] clip-z is heavily non-linear so a fixed-threshold edge
detector only caught near-camera silhouettes. Now reverses the
wgpu z-remap (z * 2 - 1 back to GL NDC) then standard reverse-
perspective to view-z.
2. Threshold scales with depth: t = EDGE_THRESHOLD * c. A 4 mm gap
between two surfaces reads the same whether it's 0.5 m or 50 m
away from the camera.
3. Multiplicative blend (Dst, Zero) with fragment output of
vec3(1 - edge). Strictly darkens, never brightens. Matches GL's
(GL_DST_COLOR, GL_ZERO) blend.
Constants EDGE_SCALE=6.0 / EDGE_THRESHOLD=0.004 are GL's tuned values.
Camera near/far hard-coded to 0.1 / 10000 (the viewport defaults);
they'll move to a small uniform when AppSettings ports across.
Pipeline state: depth-attachment-less, sample count 1, blend on, no
cull. Reuses depth_texture_'s TextureBinding usage that HiZ added.
Render pass loads the resolved main-pass colour (LoadOp_Load) and
writes back through the multiplicative blend; encoded between the main
pass and the HiZ resolve so HiZ uses the same MSAA depth that produced
the edges. edge_bind_group_ rebuilds lazily when depth_view_ is
replaced (mirrors the HiZ bind group lifecycle).
Perf cost on the 10-sidecar / 380k-instance benchmark: 0.1 ms (11.5 →
11.6 ms). Fullscreen depth-laplacian is essentially free on this GPU.
Remaining stage 9 work: HUD/labels/lines/points overlay primitives,
which need the QPainter-→-texture path. Lower visual priority than
edges; handled in a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
51dc31a50b |
wgpu backend: 10× perf — megadraw, async HiZ, parallel cull, motion mode
Closes the perf gap to the GL backend on real BIM benchmarks. On a 10-
sidecar / 380k-instance corpus at a fixed --camera the wgpu binary went
from 110.6 ms to 11.6 ms (vs GL's 23 ms — half the frame time, but
note GL is doing extra work the wgpu backend hasn't ported yet; see
the caveats list at the bottom). Bundled because the pieces interlock
and shipping any of them without the others reintroduces the same wall.
1. Cross-mesh vertex pulling (single mega-draw per model)
The previous one-drawIndexed-per-(mesh × LOD-bucket) loop was costing
~13ms on a 27k-mesh scene. CPU now emits a flat visible_draws[]
(16 B per visible (mesh,lod,instance)) plus a prefix_sums[] table.
WGSL binary-searches prefix_sums by @builtin(vertex_index) to find
the entry, then manually fetches the mesh-local index from a
storage-bound indices[] and pulls the packed 12 B vertex. No
setIndexBuffer; the shader reads everything from storage. Bind
group grew from 4 to 7 entries (vertices, meshes, instances,
indices, visible_draws, prefix_sums, per-model uniform) — well
under WebGPU's mandatory 8 storage / 12 uniform floor.
2. Async HiZ readback via ping-pong staging buffers
Sync wait via wgpuInstanceProcessEvents was costing ~37 ms on a
real scene (GPU drain). Two staging slots now ping-pong: frame N
kicks a non-blocking mapAsync on slot K, frame N+1's first action
is one processEvents drain. Pyramid is 1-2 frames stale — matches
the "slightly-stale depth, fine" pattern the GL backend already
documents. encodeHizResolve returns -1 (skip) if both slots are
in flight; cull keeps using the most recent pyramid.
3. Cull reorder: contribution before HiZ
HiZ projection is ~10× more expensive than the contribution
check, yet most contribution-survivors would be HiZ-rejected
anyway on dense scenes. Computing projected_px first lets
contribution short-circuit ~80% of HiZ tests with no rejection-
quality loss. Saved ~34 ms on the dense bench.
4. Motion-mode contribution threshold
AppSettings::motionMinPixelRadius parity. While the camera is
changing (orbit/pan/zoom/--benchmark sweep), drop instances
below 10 px instead of 2 px. Halves visible_objects during
motion with no perceived quality loss.
5. Parallel cull (std::async across models)
Per-model cullModelCpu split into Compute (CPU-only, thread-safe)
+ Upload (main-thread wgpu queue writes). std::async fan-outs the
compute across models; main-thread joins and uploads. Wall-clock
cull on the 10-model corpus drops from ~17 ms single-threaded to
~9 ms across cores.
6. --no-hiz CLI flag + per-phase benchmark timings
Benchmark now also prints "per-frame avg ms: cull=X
hiz_readback=Y" so future regressions can be attributed without
guesswork. --no-hiz toggles the master switch from the CLI.
Honest caveats — wgpu is currently faster mostly because GL is doing
work we haven't ported yet:
- Edge silhouette pass (stage 9) will add ~3-5 ms back to wgpu.
- GL's HiZ uses the BVH so it rejects whole subtrees (1.7k vs
our 358 rejects on the same scene). BVH for HiZ is future work
(task #13 / a new task) — until then we draw more sub-pixel
geometry that's behind closer surfaces. Visually correct, perf
cost paid. Stage 4+5 are unaffected.
Verified pixel-identical on basic.ifc through every change. Real-scene
visual diff against GL pending the --screenshot flag on the GL minimal
(task #10's other half).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
406124ca3d |
wgpu backend: HiZ occlusion culling
Stage 7 of the wgpu port. Per-frame after the main render pass:
1. encodeHizResolve runs a depth-only render pass that samples the
MSAA depth texture (sample 0) and max-reduces it into a small
single-sample Depth32Float target (256 × ~h-aspect). Implemented
as a fullscreen-triangle WGSL pipeline; one nested loop per
output texel over its source rect. WebGPU has no built-in depth
resolve, so this combined resolve+downsample fragment shader is
the way.
2. copyTextureToBuffer writes the small resolved depth into a
CPU-mappable staging buffer (≈ 160 KB at 256×160).
3. readbackAndBuildHizPyramid maps the staging buffer (sync via
wgpuInstanceProcessEvents — small enough that the stall is
well under a millisecond), strips per-row padding, and CPU
max-reduces a full mip pyramid (level 0 → 1×1). Stores the VP
used so the next frame can project AABBs into the same space.
Next frame, cullModelCpu calls aabbOccludedByHiz after the frustum
test: projects all 8 AABB corners through hiz_vp_, computes the
screen-space AABB and the nearest projected z, picks the mip level
where the AABB covers ≤ 2 texels per axis, samples that level's 2×2
window, and culls iff min_z > max_pyramid_depth in [0,1] z.
Plumbing changes:
- depth_texture_ gains TextureBinding usage so the resolve shader
can read it.
- hiz_enabled_ master switch defaults true; mirrors IFC_NO_HIZ in
the GL backend. Disabling skips encode + readback entirely.
- Bench output's "hiz_rej N" field now reflects actual rejections.
Verified: basic.ifc (3 instances, no occluders) renders pixel-
identical to pre-HiZ — proves the test rejects nothing it shouldn't.
Real rejection counts need a dense scene; this should drop visible-
objects count noticeably on real BIM benchmarks where back-of-room
walls hide each other.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6ce2b564c1 |
wgpu backend: per-instance contribution culling in cullModelCpu
Quick win before the proper HiZ stage. Adds a min_pixel_radius threshold (defaults 2.0 to match AppSettings::minPixelRadius() in GL): instances whose projected bounding-sphere radius falls below it are dropped from the per-mesh buckets entirely. The projected_px math (radius_world * focal_px / view_z) is now computed once per instance and shared with the LOD pick that uses the same number. Saves one square root per instance per frame on dense scenes vs the previous code path that only computed it inside the LOD branch. Expected impact on real BIM benchmarks: visible-objects count drops by roughly 10×, matching the GL backend's number. Without this fix, wgpu was drawing every frustum-surviving sub-pixel instance — most of the work and most of the geometry the GL backend wasn't even submitting. Motion-mode threshold bump (10.0 in GL during camera drag) lands later when mouse-driven motion tracking is wired up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7893135790 |
wgpu backend: --camera flag + request adapter's max buffer limits
Two pieces that block proper side-by-side parity with the GL minimal: 1. --camera tx,ty,tz,dist,yaw,pitch. Same format string as the GL minimal so a pasted camera arg lands the same view on both backends. setCamera() also flips initial_view_applied_ = true so the auto- viewAll-on-first-load doesn't snap away from the script-set position when the model finishes uploading. 2. Real BIM models exceed the conservative WebGPU defaults at device create time. A 114k-instance / 19M-index sidecar's vertex storage is 139 MB, which trips wgpu's default 128 MB max_storage_buffer_binding_ size and bind-group creation fails. Now wgpuAdapterGetLimits is called first and the device is requested at the adapter's full ceiling — every desktop driver supports multi-GB. Trade-off worth flagging: web parity will fail here because browsers cap at the defaults. The eventual fix is to split a model's vertex/ instance storage into ≤128 MB chunks with a small per-frame routing table, which is a real chunk of work. For now this unblocks all the native benchmarking the user is actually doing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f9a9273ecc |
wgpu backend: match GL orbit + viewAll math so pivots align
Reported regression: --benchmark on the same sidecar visibly rotated
around a different point in the wgpu binary than in IfcViewerMinimal.
Root cause was two camera-convention drifts:
1. orbitEye placed the camera at (sin yaw, -cos yaw) from target;
the GL backend uses (cos yaw, sin yaw). Same target, but the
camera faces a different side of the model at yaw=0, which made
the orbit feel like it pivoted around a different point even
though the actual world-space target was the same. Now exactly
matches GL ViewportWindow::updateCamera:
eye.x = target.x + dist * cos(pitch) * cos(yaw)
eye.y = target.y + dist * cos(pitch) * sin(yaw)
eye.z = target.z + dist * sin(pitch)
2. viewAll's distance was an ad-hoc 0.6 * diag / tan(half_fov);
GL uses frameAabb(mn, mx, 1.10): tan_half = tan(fov/2),
min_aspect = min(aspect, 1), distance = (radius / (tan_half *
min_aspect)) * 1.10. Aspect-aware so portrait windows pull back
enough that the bounding sphere still fits on the tighter axis.
Now ported verbatim.
Also logs the computed target + distance on viewAll so a follow-up
side-by-side run prints both backends' framings and any remaining
discrepancy is easy to spot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
244a145255 |
wgpu backend: per-instance LOD0/LOD1 pick in the cull
Stage 8 of the wgpu port. cullModelCpu now buckets each visible instance
by (mesh_id, lod) instead of (mesh_id), and emits one MeshDraw record
per non-empty bucket. LOD pick projects the instance's world-space
bounding sphere to pixels via
projected_px = world_radius * focal_px / view_z
where focal_px = viewport_h / (2 * tan(fov_y/2)) and view_z is the
forward·(center-eye) depth. When projected_px < lod1_pixel_threshold_
AND the mesh has a baked LOD1 slice (MeshInfo.lod1_index_count > 0),
the instance draws the LOD1 index range instead of LOD0; baseVertex
and the vertex storage are shared between LODs.
mesh_draws can now grow to up to 2 × meshes.size() per frame (LOD0 + LOD1
slice per mesh). The visible_buffer layout per mesh becomes
[LOD0 instances | LOD1 instances] contiguous, with each MeshDraw
referencing its own firstInstance offset.
lod1_pixel_threshold_ defaults to 30 (mirrors AppSettings::
lod1PixelThreshold() in the GL backend); set to 0 to disable LOD1
entirely (always LOD0). AppSettings port lands in a later commit.
Verified: basic.ifc (3 tiny instances, no LOD1 baked by meshoptimizer
since each mesh is well under the 500-tri threshold) renders pixel-
identical to pre-stage-8 — proves the all-LOD0 path is preserved.
Real LOD switching needs a sidecar where buildLods produced LOD1 slices.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4596f2e584 |
wgpu backend: lighting parity, MSAA, cavity shading, fix sRGB output
Closes the visible gap to BonsaiViewer down to just the post-process edge silhouette pass (still pending in task #9). Four changes bundled because together they bring up the parity story: - WGSL fragment now applies cavity = clamp(length(fwidth(n))*1.5, 0, 0.35) and multiplies by (1 - cavity). Matches GL shader. - Lighting constants switched to GL's exact values: key (0.3, 0.5, 0.8), fill (-0.3, -0.5, 0.8), sky tint (0.55, 0.60, 0.70), ground tint (0.35, 0.32, 0.28). My initial guesses were close but not identical; matching them means side-by-side diffs only flag actual pipeline differences, not lighting tweaks. - 4× MSAA: render pass writes into a MULTISAMPLE color attachment (surface_format_-matched), resolves into the surface texture for present. Depth is also 4 samples. Pipeline.multisample.count = 4. ensureMsaaColorTexture / releaseMsaaColorTexture mirror the depth- texture lifecycle. Matches GL minimal's QSurfaceFormat::setSamples(4). - sRGB output fix. wgpu-native's Vulkan swap chain on X11 treats BGRA8Unorm as sRGB-output (applies linear→sRGB encoding on shader writes), even though caps.formats[0] reports plain Unorm. The GL backend writes to a non-sRGB framebuffer with no such conversion, so a clearValue of (0.125, 0.137, 0.161) lands as bytes (32, 35, 41) on GL but (99, 104, 112) on wgpu — ~3× brighter. Pre-decoding via srgbToLinear on (a) the clearValue in C++ and (b) the final fragment colour in WGSL makes wgpu's implicit encode round-trip, so the final bytes match GL. Verified via screenshot pixel sample: #202329 background reads as exactly (32, 35, 41). Remaining visible gap to BonsaiViewer is the dark-line edge silhouettes (renderEdgePass in GL, depth laplacian → outline). That belongs with the overlay / post-process work in task #9. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a95437dd64 |
wgpu backend: match GL pitch sign so drag-down tilts the camera up
Drag-down was decreasing pitch (camera diving), opposite to the GL viewport's convention where drag-down increases pitch so the top of the object rotates toward the viewer. Yaw direction was already correct. Matches the existing user muscle memory from IfcViewerMinimal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
de44de26f8 |
wgpu backend: clearer sidecar-load diagnostics + tilde expansion
The single "(file missing, wrong magic, or schema mismatch)" message
was making triage harder than necessary. loadSidecar now expands a
leading ~/ (shells skip it inside double quotes, which trips up paste-
from-launcher), and on failure peeks the file's header itself to
report exactly which check failed:
- "Sidecar not found" — file doesn't exist
- "Sidecar unreadable" — exists but open failed
- "Sidecar truncated" — <12 bytes
- "Sidecar magic mismatch" — wrong magic, reports got vs expected
- "Sidecar schema mismatch" — wrong version, reports both numbers
and suggests re-baking
- "Sidecar endianness mismatch" — cross-platform load attempt
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
819196b3ce |
wgpu backend: --benchmark N parity with the GL minimal
Stage 11 of the wgpu port. WgpuViewportWindow gains setBenchmarkFrames(N); the minimal driver wires it to a --benchmark N flag. Renders N frames after a 5-frame warmup, yaw-sweeping the camera at 0.5°/frame, captures per-frame wall time with QElapsedTimer (cull + encode + present), and prints avg/median/p1/p99 + last-frame stats in the same line format as IfcViewerMinimal so a script can diff them line for line. Per-frame stats (visible_objects, visible_triangles, sub_draws) are now summed in render() from m.mesh_draws. hiz_rej reports 0 until stage 7 adds HiZ occlusion. Verified on basic.ifc (3 instances): wgpu 11.68 ms avg vs GL 11.75 ms avg — same scene, same camera sweep, same window size. Noise-level delta as expected on a tiny scene; the interesting comparison is on real BIM corpora once you bake them to v13 sidecars. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ddef8c65b5 |
wgpu backend: orbit/pan/zoom mouse navigation
LMB drag → orbit (yaw/pitch, pitch clamped to ±89.9° to avoid gimbal flip at the poles). MMB drag → pan in the camera's screen-space plane, world-units-per-pixel sized against the view frustum at the pivot depth so panning feels constant regardless of zoom. Wheel → zoom (12% per notch, sign matches "wheel up = closer"). LMB is bound to orbit because selection isn't wired yet; will rebind to selection + nav preset once AppSettings ports over. Pure addition to WgpuViewportWindow — overrides four QWindow event handlers, no changes to render or cull paths. Lets you actually fly around a loaded sidecar without a screenshot loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
61726e00a4 |
wgpu backend: CPU frustum cull + per-mesh draw compaction
Stage 6 of the wgpu port. Replaces the one-draw-per-(mesh, instance) loop
with a CPU cull pass that survives one drawIndexed per non-empty mesh
with packed instanceCount.
Adds to WgpuModelGpuData:
- visible_buffer: u32[] storage SSBO, pre-sized to instance_count at
applyCachedModel so the bind group reference never invalidates.
Re-uploaded each frame via wgpuQueueWriteBuffer.
- mesh_draws: per-mesh schedule (first_instance, instance_count,
first_index, base_vertex, index_count). instance_count==0 means the
mesh contributed nothing this frame and the draw is elided entirely.
cullModelCpu per-frame:
- Extract 6 frustum planes from the same VP we write into the uniform.
WebGPU clip-space z is [0, 1], so near plane = matrix row 2 (not
row 3 + row 2 as in GL); rest of the derivation is standard.
- Per-instance AABB-vs-frustum test using the p-vertex shortcut
(cheapest correct early-out for AABBs).
- Bucket survivors by mesh_id; flatten into a contiguous u32 list;
upload via wgpuQueueWriteBuffer. Per-mesh slice is [first_instance,
first_instance + instance_count).
WGSL adds @group(1) @binding(3) var<storage, read> visible: array<u32>
and an extra indirection: instance_idx = visible[iid]; the rest of the
shader is unchanged. firstInstance on each drawIndexed offsets into
visible[], so each mesh reads its own slice.
Verified two ways:
1. basic.ifc (3 instances, all on-screen) renders pixel-identically
to pre-stage-6 — proves cull keeps everything it should.
2. basic.ifc + a synthetic instance placed at (100, 100, 100) is
culled cleanly: only the cube renders, the far quad is rejected
by the frustum test. Proves cull actually rejects out-of-frustum
geometry rather than passing everything through.
Contribution culling, HiZ, and LOD selection arrive in stages 7 and 8;
they all hook into the same cullModelCpu seam.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
75b9963136 |
wgpu backend: --screenshot capability for visual verification
Pulls the capture half of task #10 forward so we stop flying blind from stage 3 onward. WgpuViewportWindow gains captureNextFrameToPng(path); the minimal driver wires it to a --screenshot PATH flag that renders one frame, copies the surface texture back to host memory, writes a PNG via QImage, and quits. CopySrc is added to the surface configuration usage so the surface texture can be the copy source. The texel-to-buffer copy honours WebGPU's 256-byte bytes-per-row alignment by padding rows and stripping the padding when assembling the QImage. Surface format 28 (BGRA8Unorm) is byte-swapped to RGBA on the way into QImage::Format_RGBA8888; RGBA8 surface formats are memcpy'd straight through. Verified end-to-end on /tmp/basic.ifcview: 3 cube meshes/instances render with depth, back-face cull, and the hemisphere-ambient + key+fill lighting model — top face reads sky (bright), front faces read mid-tone, exactly as the WGSL shading intended. The pixel-diff half of task #10 (comparing against a GL baseline) lands later when the GL minimal binary gets an equivalent flag. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
bbf2bfde92 |
wgpu backend: vertex-pulling main render pass
Stage 3 of the wgpu port. Replaces the clear-only render loop with the
full main shading pass:
- WGSL port of the GL main shader. Vertex-pulling: the vertex storage
buffer is read as array<u32> in the shader, with pos/normal/color
decoded manually per vertex. baseVertex (set per draw to mesh's
vertex offset) folds into @builtin(vertex_index) automatically;
firstInstance carries the instance slot for @builtin(instance_index).
No vertex-input layout — vertex pulling means no IA bindings.
- Render pipeline bound to depth-32-float (write-on, less compare),
back-face cull, CCW front face. Pre-multiplies a [-1,1]→[0,1] z-remap
matrix onto Qt's projection so WebGPU's clip-z convention is met.
- Two bind groups: group=0 per-frame (uniform with view-proj + key/fill
light + hemisphere ambient), group=1 per-model (three read-only
storage buffers: vertices, mesh quant, instances).
- Depth texture is created lazily and recreated on surface resize.
- Orbit camera state on WgpuViewportWindow with viewAll() that frames
the union of all loaded models' world AABBs after the first load.
Mouse navigation lands later.
- Draw loop: one drawIndexed per (mesh, instance) pair per model. This
is correct but CPU-heavy on dense scenes; stage 6 introduces the cull
+ compacted visible list that lets multiple instances of one mesh
collapse to a single call, and the eventual GPU-driven cull (post
sunset of the GL backend) goes further.
Verified on /tmp/quad_v13.ifcview (1 mesh, 1 instance) and on a real v13
sidecar baked from basic.ifc via the GL minimal viewer (3 meshes,
3 instances, 864 B verts). No wgpu validation errors fire across pipeline
creation, depth attachment, bind groups, or the draw loop on either.
Visual confirmation deferred until --screenshot lands (task #10) which
is being pulled forward next so we don't keep flying blind.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9daa5fe195 |
wgpu backend: load .ifcview sidecars onto GPU buffers
Stage 2 of the wgpu port. WgpuViewportWindow gains a queueLoadSidecar API (called from the minimal driver before init) and an applyCachedModel that runs after init: reads via SidecarCache::readSidecar, allocates four wgpu buffers per model (vertex storage, index, mesh-quant storage, instance storage), uploads via wgpuQueueWriteBuffer, retains a CPU mirror of the MeshInfo/InstanceCpu arrays for the cull and picking paths that arrive in later stages. MeshGpu (the per-mesh quantization basis) is derived from MeshInfo on the fly; InstanceGpu (transform + ids) is derived from InstanceCpu and uses the cached float transform — composing from placement_transformation against federation-stage matrices lands when stage 5 wires those. SidecarCache.cpp is compiled into IfcViewerWgpu directly: it's pure C++ with no Qt/OCCT/IFC-parse deps, so dragging in the IfcViewer static lib for one source file would be wasteful. This duplication goes away once src/ifcviewer-core/ is extracted (task #12). Verified on a synthesised v13 sidecar (4 verts, 6 indices, 1 mesh, 1 instance) and a multi-sidecar load that assigns successive model_ids. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
19a39a0413 |
Scaffold experimental wgpu viewer backend
Adds src/ifcviewer-wgpu/ and src/ifcviewer-wgpu-minimal/ behind a new BUILD_BONSAIVIEWER_WGPU option (default OFF), gated independently of BUILD_BONSAIVIEWER. Stage 1 brings up a Qt window with a wgpu-native v29 surface (X11) and clears to the background colour — no rendering beyond that yet. Mirrors the lifecycle of the GL ViewportWindow so subsequent stages (vertex-pulling renderer, pick, cull, HiZ, overlay) slot in without restructuring the host. wgpu-native is fetched as a pre-built binary release via FetchContent; its .so SONAME is patched in at configure time so dependents get a clean DT_NEEDED. The X11 native handle is obtained via the public QNativeInterface::QX11Application API; Wayland and macOS/Windows surface creation are stubbed with explicit "not wired yet" warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |