mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-17 10:59:17 +00:00
d45174066feddbf13a349bd04cc79e35ceedec64
19835 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
49348908e6 |
Add wall-fillet helper functions + recreate_wall hook
Eleven module-level helpers in wall.py that the upcoming wall-fillet operators + gizmo groups depend on. Each is self-contained or references only helpers earlier in the file; the operators and gizmos themselves land in follow-up commits. * _wall_fillet_props / _wall_fillet_preview_active / _wall_fillet_preview_walls: thin read-side accessors over the BIMPreviewProperties.wall_fillet pointer (added with the operators commit). Safe today: get_preview_props returns None until the pointer is attached. * _walls_have_zero_slope_for_fillet: validates that input walls are vertical (x_angle ~ 0); slanted-extrusion fillets require swept-along-curve geometry the banana profile builder doesn't support. * _build_curved_corner_body_representation: builds the banana (annular sector) IfcExtrudedAreaSolid as a polyline-tessellated IfcIndexedPolyCurve. * _apply_fillet_corner_geometry: positions the corner wall at tangent_a and rebuilds its body. Shared by the creation operator and the regenerate path. * _resolve_two_walls: pulls (active, other) from a 2-wall selection, validates both as LAYER2 + straight-axis + not-already- a-fillet-corner. * _pick_dominant_wall_material: returns the thickest layer's material from an element's IfcMaterialLayerSet / Usage. * regenerate_fillet_corner_wall: re-runs the geometry build from BBIM_Wall.FilletRadius + current neighbour layer parameters. Called by tool.Model.recreate_wall when the IsFilletCorner pset is set; the FIXME(PR4) placeholder in recreate_wall is dropped. * _wall_fillet_gizmo_x_matrix: 4x4 placement matrix with local +X aligned to a world-space direction; used by the fillet preview gizmo group. Centralises the IsFilletCorner pset read as tool.Parametric.is_fillet_corner_wall — replaces 3 inline get_pset(element, "BBIM_Wall", "IsFilletCorner") sites (tool.Model.recreate_wall, tool.Model.recalculate_walls, tool.Parametric.is_path_connectable_wall) plus the new _resolve_two_walls call. Generated with the assistance of an AI coding tool. |
||
|
|
c250b2c1a7 |
Gate parametric-edit array gizmo until integration completes
The framework's parametric-edit icon row currently binds an array icon to bim.add_array_from_feature_edit, but the supporting per- feature add-array flow and gizmo positioning haven't fully landed. Showing the icon today lets the user click it and trigger a half- wired flow. Force the icon hidden inside the props.is_editing branch of BaseParametricGizmoGroup.update_editing_gizmos. The else-branch (not editing) already hides it, so this just mirrors that behavior during edit mode. Drop this gate when array integration completes to re-enable the icon position + visibility plumbing. Generated with the assistance of an AI coding tool. |
||
|
|
6874d52100 |
Add cursor-aware extend-arrow flip on wall edit gizmos
The extend-X / extend-Z icons in GizmoWallEdition's cursor row are billboarded toward the camera; without orientation polish they always point in the same screen-space direction regardless of which wall endpoint the click will move (or whether the cursor sits above or below the wall top). New helper mirrors the icon's local-X (extend-X) or local-Y (extend-Z) axis so each arrow points toward the end it will move: * Extend-X: walk wall midpoint to figure out which endpoint stays fixed (cursor past midpoint → ATSTART stays; cursor before midpoint → ATEND stays). Project the fixed endpoint into screen-space and flip the arrow when the gizmo's anchor sits on the same side. * Extend-Z: flip when the cursor is below the wall top (within EXTEND_FLIP_EPSILON tolerance). Called once per resolved cursor gizmo from ``GizmoWallEdition._update_cursor_gizmos``, after the gizmo's ``matrix_basis`` is set by ``gizmo.billboarded_at``. Reuses ``gizmo.should_flip_extend_arrow`` + ``EXTEND_FLIP_MIRROR_X/Y`` + ``EXTEND_FLIP_EPSILON`` already on tool. Generated with the assistance of an AI coding tool. |
||
|
|
6c21e2b6f4 |
Add single-wall unjoin operator + gizmo group
GizmoWallJoinIntersection's unjoin only fires when exactly two walls are selected and surfaces one icon at their shared corner — useless when the wall has 3+ joins and the user wants to disconnect just one. * UnjoinWallPathConnection: surgical counterpart to UnjoinWalls. Disconnects the active wall from a single partner wall identified by IFC GlobalId (invariant under Blender-object renames + file save/reload + undo). Walks both inverse arrays of the active wall for the specific IfcRelConnectsPathElements joining the pair — matches DumbWallJoiner.split's pattern and avoids disconnect_path's direction-sensitivity. Resyncs both walls' draft props after the recreate_wall pass. * GizmoWallUnjoinSingle: activates on exactly-one selected LAYER2 wall. Preallocates a pool of 16 unjoin icons (Blender forbids gizmo allocation outside setup(); ATSTART + ATEND + ATPATH rels are rarely more than a handful). Per-frame, iterates _iter_path_connections, positions one billboarded icon at each join via tool.Wall.path_connection_location_world, and hides the rest. Each visible icon's bound operator carries the partner GlobalId, so a click removes only that one rel. * model/__init__.py: register both classes alphabetically. Mutually exclusive with GizmoWallJoinIntersection via poll() — that group requires len(selected) == 2; this one requires 1. Generated with the assistance of an AI coding tool. |
||
|
|
70845e4dd4 |
Add wall path-connection inverse-walk helpers
The single-wall unjoin gizmo needs to enumerate every IfcRelConnectsPathElements a wall participates in, regardless of which side of the rel the wall was authored on, and place an icon at each join's physical location. Two helpers carry that work: _path_connection_location_world wraps core.compute_path_connection_location at the Vector boundary. _iter_path_connections walks ConnectedTo + ConnectedFrom, normalises orientation to (other, self_ct, other_ct), and filters non-wall partners + None refs so per-frame gizmo positioning survives malformed IFC. Generated with the assistance of an AI coding tool. |
||
|
|
c7fba1abb8 |
wgpu streaming: screen-space AABB priority + grace period + interactive heartbeat
The chunk priority metric is now the 2D projected pixel area of the chunk's AABB on screen — 8 corners projected through view-projection, 2D axis-aligned bbox of the projected points, clamped to viewport. This replaces the prior bounding-sphere-radius² metric, which was a 3D approximation: it treated a 322 × 55 × 5 m slab as a 163 m sphere, giving it the same huge priority face-on or edge-on. The new metric genuinely answers "what would this chunk's AABB cover if rendered solid given the current camera and viewport." Newly-loaded chunks get a 30-frame grace period at full priority (visibility_history floor temporarily forced to 1.0). Without it, just-loaded chunks crashed to history=0 → effective priority = pri × 0.05 → immediately reverse-swapped by the chunk they displaced. Cycle starved the per-frame load budget so candidates ranked below the cyclers never got attempted. 30 frames = HISTORY_ALPHA's time constant — enough for visibility_history to develop meaningfully. EVICT_PRIORITY_RATIO bumped 1.21 → 2.0 to suppress more swap noise between similar-priority chunks. Interactive heartbeat log added: every render in non-bench mode prints [frame] with fps, ms, obj, sub_draws, hiz_rej, cull, stream, chunks breakdown (resident/frustum/total + missing count), VRAM, model count. Every 30 frames when something's missing, also dumps: - top 8 models by missing chunk count - top 20 missing chunks by priority (with AABBs) - bottom 5 residents by effective priority - all chunks of brace.ifc (one-off diagnostic, hardcoded for the brace-visibility investigation) The heartbeat made the streaming bug visible: a brace model that isolation-loads correctly is missing in the full set because slabs covering more pixels win the priority contest. Per-model fairness or manual pinning are the remaining options if pixel-area + grace + hysteresis isn't enough — left for follow-up so the user can decide based on real testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d7b5ac1453 |
Add wall draft-resync helper + wire 6 mutation operators
After a one-shot wall IFC mutation (unjoin / split / merge / extend / join-at-corner …) the always-visible gizmos on the OTHER side of the join can be left reading stale ``BIMWallProperties`` — the IFC geometry moved but the draft props that drive the gizmo handles still point at the pre-mutation numbers, so a subsequent edit-mode enter shows the wall at its old length / position. * New ``_maybe_resync_wall_props_from_ifc(obj)``: re-primes a single wall's draft props from current IFC, with guards for non-walls, non-parametric walls, and walls in an active draft session (the draft is then the source of truth, not IFC). Must run from an operator ``_execute`` — ID writes from gizmo refresh raise. * New ``_resync_walls_after_mutation(objs)``: iterates the above across a selection. * Six existing mutation operators gain a resync call after their ``core.*`` / ``DumbWallJoiner`` mutation completes: UnjoinWalls, ExtendWallsToUnderside, ExtendWallsToWall, SplitWall, MergeWall, JoinWallsIntersection. MergeWall resyncs only the surviving wall — the active wall is the deletion target. Generated with the assistance of an AI coding tool. |
||
|
|
1961cd905e |
Fix parametric framework live-session regressions
Bundle of bugs surfaced when exercising the new gizmo framework end-to-end in a live Blender session after the bim/module/drawing/gizmos.py refactor + TypeAccessor/CycleType/PickType mixins landed. Register / annotation resolution * parametric_lifecycle.py: hoist `entity_instance` import out of TYPE_CHECKING so typing.get_type_hints resolves the Callable[[entity_instance], bool] annotation at operator registration (CycleDoorType, CycleWindowType, CycleStairType failed with NameError). Clarify the INTERFACE return contract on the picker entry-point so readers see why the gizmo step stays off the undo stack. Framework callable contracts * model/wall.py, door.py, window.py, stair.py: migrate `props_getter` and `element_checker` from bl_idname strings to bound classmethods on tool.Model / tool.Parametric. BaseParametricGizmoGroup.get_props expects a callable; the string form raised TypeError on first gizmo poll. * model/door.py, model/stair.py: drop the dead `prop_path=` operator kwarg from create_arc_gizmo / create_icon_gizmo call sites. The framework helper blindly setattrs every kwarg onto the operator's OperatorProperties, but ToggleDoorSwing / ToggleStairProperty don't declare prop_path — the setattr raised mid-setup_element_specific_gizmos, so self.gizmo_door_type / self.lock_gizmo never got assigned and every subsequent draw_prepare tornadoed AttributeError. Nothing reads op.prop_path anywhere; the kwarg was dead data. Dispatcher operators * model/array.py: add EnableEditingParametric (the framework pen-icon dispatcher that routes to a per-feature edit operator by bl_idname string) and AddArrayFromFeatureEdit (binds the framework's array icon to bim.add_array on the current parametric draft). * model/__init__.py: register both new operators. Per-frame robustness * drawing/gizmos.py: guard BaseParametricGizmoGroup.draw_prepare with is_setup_complete() — matches the existing guard in refresh() and in BaseSchematicGizmoGroup.draw_prepare(). Defense-in-depth: when any subclass's setup raises mid-way, draw_prepare now no-ops cleanly instead of per-frame AttributeError-tornadoing on whatever attribute the failed setup phase was meant to populate. * model/decorator.py: guard ProfileDecorator.__call__ against context.active_object is None. The decorator is a per-frame viewport draw handler; deselecting or deleting the active object while it's installed crashed on obj.mode access. Treat None the same as "no longer in edit mode" — uninstall + fire the exit callback if present. * geometry/data.py: ViewportData.load() populates `data` before flipping `is_loaded`, so a raise from cls.mode() no longer leaves the class flag-set but data-empty for subsequent reads. Generated with the assistance of an AI coding tool. |
||
|
|
3d368e0079 |
wgpu chunks: 3D Morton-code spatial sort (tight voxel chunks)
The previous chunk-plan sorted meshes by lexicographic (z, y, x) centroid — effectively a 1D Z-slab traversal. On a typical IFC building (50 × 50 × 100 m), a 16-MB chunk's 80-ish meshes spanned roughly 50 × 50 × 0.5 m. On a city federation it was much worse: the first chunk grouped ground-floor stuff from every building, spanning the entire scene horizontally. Per-chunk AABBs that wide make frustum / contribution / HiZ rejection useless (every chunk "overlaps the frustum" by virtue of spanning the whole scene). 3D Morton (Z-order) interleaves bits of quantised (x, y, z) centroids, so consecutive items in the sorted order cluster in all 3 axes — chunks become tight 3D voxels of the model. Prerequisite for the contribution-aware eviction priority (task #25) to actually discriminate near and far chunks. 21 bits per axis = ~2 M bins per axis, sub-millimetre precision on a kilometre-scale scene. Both apply paths (streaming and non- streaming) share the same sortMeshIdsByMorton helper. Benchmark unchanged (~47 fps avg, 20 ms cull, 0.3 ms stream) — the distance-based evictor still keys on chunk centres, which moved slightly under Morton but not enough to materially shift residency. The user-visible win comes from the next commit, which switches priority to screen-space contribution × HiZ history — both of which need today's tight AABBs to mean anything. Pixel-identical to non-streaming on basic.ifc on both paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0ec72482c2 |
wgpu pool: halve-on-failure in addSubBuffer extracts +35% VRAM
Many Vulkan drivers cap a single VkDeviceMemory allocation at exactly
maxStorageBufferBindingSize (NVIDIA: 2 GB on consumer GeForce) or
refuse big contiguous allocations once heap is fragmented. The old
addSubBuffer gave up at the first refusal, latching growth_disabled_
— so on a 4 GB GeForce we extracted 2 GB and called it done.
The wgpu-mem-probe tool (
|
||
|
|
feab05650d |
wgpu-mem-probe: standalone tool to investigate driver VRAM ceilings
New headless wgpu probe app — no Qt, no surface, just initializes a device and stress-tests buffer allocations. Reports: 1. Adapter + device limits (maxBufferSize, maxStorageBufferBindingSize). 2. Single-allocation probe: descending sizes, each released, finds the largest single buffer the driver will grant. 3. Cumulative probe: halve-on-failure, finds total VRAM the runtime will let us park behind one device across multiple sub-buffers. 4. Fixed-size cumulative probe: 1 GB / 512 MB / 256 MB uniform sizes, to detect whether the "big-first" strategy leaves VRAM on the table. Findings on a GTX 1650 (4 GB physical) + wgpu-native + Vulkan: - maxStorageBufferBindingSize = 2 GB (driver cap, not wgpu-native). - Any single storage buffer > 2 GB is REFUSED. - Total available across N sub-buffers = ~3 GB, INVARIANT under allocation pattern (2+1+0.06, 3×1 GB, 6×512 MB, 12×256 MB all reach 3.00 GB). Driver hands out a fixed VRAM slice; pattern doesn't matter. - Remaining ~1 GB is held by the desktop compositor + OS. - GL's higher "4 GB+ resident" claim is overcommit into host RAM, which wgpu/Vulkan don't do. The +50% (2 → 3 GB) improvement is real and worth chasing — a follow-up halve-on-failure addSubBuffer in WgpuBufferPool will extract that on this card. On 8/16/24 GB GPUs the same code gets us proportionally more. Build: ninja -C build-viewer-wgpu WgpuMemProbe Run: ./build-viewer-wgpu/wgpu-mem-probe/WgpuMemProbe Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
dcc2bf1c01 |
wgpu streaming: background-thread chunk I/O kills render-thread stutters
The sync chunk-read on the render thread was causing 100-300 ms spikes during orbit whenever a new chunk needed to scatter-gather its mesh bytes from disk. p99 was 326 ms on the close-camera benchmark. New WgpuStreamingThread: one worker thread with a condvar-protected request/result queue. driveStreamingLoads becomes drain-then-enqueue: 1. Drain any results the worker pushed since last frame. For each, pool-allocate slices + queueWriteBuffer + build the chunk bind group (still main-thread because wgpu queue ops aren't thread-safe). 2. Walk visible non-resident chunks (sorted by distance), evict to make pool room, and enqueue the request. Chunk gains is_loading flag to prevent re-enqueueing while in flight. loadChunkBytesAndUploadGpu becomes the sync fallback path, used only when a screenshot is pending — the deferred-capture wait would otherwise let the window manager re-layout the window between frames and the test framework would capture at the wrong size. Normal streaming always goes through the worker. Bench warm-gate / requestUpdate gating updated to consider streaming_thread_.inFlightApprox() so we don't declare "converged" while a worker read is still in flight, and the render loop stays alive until the worker queue is empty. Refactored loadChunkBytesAndUploadGpu into two helpers: - makeChunkRequest: builds the worker request from chunk metadata - applyStreamedChunk: pool.alloc + queueWriteBuffer + bind group Both the sync and async paths share applyStreamedChunk. Benchmark (big federation, --streaming): close camera: avg 24 fps p99 47 ms (was 27/326) default camera: avg 24 fps p99 46 ms (was 31/186) stream time: ~2 ms (was 8-12) cull is now the bottleneck (20 ms median) — task #17 (GPU compute cull) is the next frontier. Pixel-identical to non-streaming on basic.ifc on both paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6f66d08bee |
wgpu cull: chunk-level frustum cull replaces BVH walk
cullModelCpuCompute previously had two paths: a flat linear scan over
all instances (default), or a BVH-stack walk (--bvh, gated off because
it regressed on dense scenes — the BVH built per instance but its
interior-node AABBs spanned huge chunks of model so most subtrees
straddled the frustum and the walk overhead beat the rejection win).
With spatial chunk planning (commit
|
||
|
|
4d36174200 |
wgpu streaming: spatial chunk planning + coalesced multi-range reads
Chunks are now grouped by world-space centroid instead of mesh-id
range, so each chunk's AABB tightly bounds its geometry instead of
spanning the whole model. Distance-based eviction can finally
distinguish the near corner of a skyscraper from the far corner.
Algorithm:
1. Compute each mesh's centroid = mean of its instances' world AABB
centres.
2. Sort mesh indices lexicographically by (z, y, x) centroid. Stable
sort keeps mesh-id order as tiebreaker for instanced repeats.
3. Greedy-pack sorted meshes into chunks ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT.
4. Each Chunk stores its mesh_ids list; the per-mesh layout (chunk_local
base_vertex / ebo_first_u32) is computed by walking the list at plan
time.
Loader: chunk vertex/index bytes are no longer file-contiguous, so
streaming uses new multi-range read paths
(readSidecarVertexRanges / readSidecarIndexRanges). Each range list
is sorted by file offset and adjacent ranges coalesced with a 64 KB
gap tolerance — on the close-camera benchmark this brings the
per-chunk seek count back down to ~mesh-id-grouping levels, so the
spatial sort costs ~nothing on I/O while delivering tighter AABBs.
Non-streaming applyCachedModel mirrors the spatial plan but gathers
from in-memory data.vertices / data.indices via per-mesh
queueWriteBuffer calls at chunk-local offsets.
Chunk struct drops vertex_byte_offset and index_first_u32 (no longer
meaningful — each chunk is N scattered ranges). vertex_byte_size and
index_count stay as aggregates for pool sizing + eviction math.
Tuning: kept WGPU_CHUNK_VERTEX_BYTES_LIMIT at 128 MB. Tried 8 MB and
32 MB; both gave tighter AABBs but the scatter-gather I/O cost blew
up because the per-frame load count grows linearly as chunks shrink
(orbit shifts the working set faster across finer chunks). 128 MB +
coalescing is the empirical sweet spot pre-v14. Once sidecar v14
re-orders bytes on disk to match spatial chunks, we can drop the
limit to ~8 MB for sharp eviction without re-paying the seek cost.
Benchmarks (big federation, --streaming):
close camera: avg 36 fps median 53 (was 35/49) — parity
default camera: avg 33 fps median 47 (was 40/49) — small regression
likely from increased coalesce overhead on more-
scattered orbit traversals; will resolve with v14.
Pixel-identical to non-streaming on basic.ifc on both paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
c3a55d7f7b |
wgpu streaming: multi-pool growth, frustum-only residency, sorted convergence
Five interlocking fixes that take --streaming on the big federation scene from "5 fps + endless flicker + infinite cold-load" to a stable 35-49 fps with a converged working set. 1. Multi-sub-buffer WgpuBufferPool. Pool now grows lazily by adding sub-buffers of per_sub_buffer_capacity_ when alloc demand exceeds existing free runs. Each Slice carries (buffer, offset, size, sub_idx). On driver refusal of addSubBuffer, growth_disabled_ latches so subsequent allocs don't keep retrying and log-spamming. pool_can_fit consults can_grow() to know when growth could rescue a candidate vs when eviction is the only path. 2. Split cull / stream benchmark timers. The previous "cull[wall]" metric was actually cull + driveStreamingLoads, blaming the wrong subsystem (~170 ms of "cull" was synchronous disk I/O). 3. frustum_visible_count on Chunk, populated in cullModelCpuCompute right after the per-instance aabbInFrustum check. driveStreamingLoads now keys residency on this instead of total_visible_draws (which includes contribution + HiZ). HiZ visibility flips frame-to-frame as occluders shift; using it for residency caused chunks to be evicted then immediately re-loaded, every frame, even with a stationary camera — both the perf cliff and the visible flicker. 4. Distance-sorted candidates in driveStreamingLoads. Walk the non-resident frustum-visible chunks in distance order (closest first). With sorted processing, evict_farthest_than converges monotonically: each swap replaces a far resident with a closer candidate; once the next candidate is farther than every remaining resident, the loop exits. Without sorting the loader visited candidates in model/chunk-id order, swapping random chunks every frame without ever converging. 5. 10% eviction hysteresis (EVICT_DIST2_RATIO = 1.21). On scenes where many chunks are clustered at similar distance from the camera (e.g. several chunks all ~370 m away), naive "evict any resident strictly farther than candidate" triggers sub-meter swaps every frame, never resting. Requiring the victim to be 10% farther in linear distance kills these cycles while still allowing genuine "much closer" candidates to evict. Plus: latched bench_warm_done_ on the cold-load gate, with a 5-frames-of-zero-loads convergence test (default-camera big scene converges in 20 frames) and a 600-frame timeout fallback that prints exactly once. Measured on the test federation (111 sidecars, ~3 GB raw, 1 M instances) with the user's close-in camera: - avg 35 fps (was 5), median 49 fps (was 7) - cull 19 ms (now the bottleneck), stream 5-8 ms (was 172) - p99 184 ms — occasional big-chunk load on the render thread; background-thread I/O would smooth that out as a follow-up. With the default wide camera: - avg 40 fps, converges in 20 frames, residency grows naturally from 59 → 76 chunks as orbit shifts the frustum. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
502c29fbc2 |
wgpu: probed-size pool replaces per-chunk createBuffer
Drops the per-machine "guess the OOM ceiling" budget knob in favour of a single buffer pool whose capacity is *probed* at device-init time. The runtime answers the question: descend from min(maxBufferSize, 4 GB) through OOM error scopes, accept the largest size that allocates cleanly. On a desktop wgpu-native v29 box this lands at 2 GB; on browser-class platforms it'll land at 256 MB – 1 GB depending on the implementation. Same code path either way. Architecture: - WgpuBufferPool (new): single WGPUBuffer + free-list sub-allocator with adjacent-range coalescing and first-fit. 256 B alignment for storage-binding offsets. - Chunks now hold (pool_vertex_offset, pool_vertex_size) and (pool_index_offset, pool_index_size) instead of per-chunk WGPUBuffer handles. Load = pool.alloc + queueWriteBuffer. Unload = pool.free. - Bind groups bind pool_.buffer() at the chunk's specific (offset, size) for both the vertex and index storage bindings. - Eviction queries pool.largest_free_run_bytes() instead of a tracked budget; the two-phase LRU/distance evictor's policy is unchanged. What this fixes: - No more gpu-alloc-rs fragmentation OOM: one VkDeviceMemory block instead of N per-chunk blocks with rounding overhead. On the test dataset (~3 GB on disk, 562 k visible instances) the wgpu backend now runs through to render without OOM at any point. - No --streaming-vram-mb knob, no hardcoded budget constant, no per-machine calibration. The pool size adapts to whatever the runtime grants. Notes: - Error scope probing: wgpu-native v29 classifies "Not enough memory left" as WGPUErrorType_Validation, not OutOfMemory. We push both filters (nested) and treat either firing as probe failure. - The 4 GB probe cap is principled, not magic: above that, wgpu-native's advertised maxBufferSize is sometimes a sentinel (1 TB) that just forces wasteful halving steps. 4 GB is the largest buffer any realistic WebGPU implementation will grant a single allocation today. - Pool destroy()/release happens after model release in shutdown() so the underlying buffer outlives every bind group that references it. Follow-ups: spatial chunking (task #22) for finer eviction granularity; cull perf needs work at 100+ models / 1M+ instances (separate from streaming concerns). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
71e61dd8a5 |
wgpu streaming (5/4): per-chunk indices + LRU/distance eviction (stopgap)
Defers index buffers per-chunk (alongside vertex bytes) so streaming fully delivers on its "don't load until visible" contract — the previous per-model index buffer was upfront-loaded and tipped scenes >~1.5 GB into allocator OOM at frame 1. Adds residency tracking + a two-phase evictor: (1) drop LRU non-visible chunks first, (2) if everything resident is visible-this-frame, drop the farthest-from-eye chunk only when the candidate to load is closer. This gives monotonic convergence to "closest visible chunks fit the budget" instead of "first 4 win, rest never load." Default budget set to 1 GB — explicitly a stopgap, documented inline. The per-machine OOM ceiling on wgpu-native (caused by allocator fragmentation from one VkDeviceMemory per createBuffer call) cannot be solved by tuning this knob. The proper fix is a probed single-pool buffer with sub-allocation, tracked under task #16. Caveat: LOD1 indices are now force-disabled when chunking — per-chunk buffers only carry LOD0. Re-enabling needs LOD1 to participate in the chunk plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5a3e5167df |
wgpu streaming (4/4): per-frame chunk-on-visible loader
The OOM fix for vertex storage. With --streaming, chunks now load on
demand:
- After cull determines which chunks have visible draws, driveStreamingLoads
walks non-resident chunks with total_visible_draws > 0 and brings up
to MAX_STREAMING_LOADS_PER_FRAME (currently 4) into residency.
- Each load: readSidecarVertexChunk → createBufferWithData →
buildChunkBindGroup → is_resident = true. Same frame's draw loop
picks up the newly-built bind_group and renders the chunk.
- If more non-resident-but-visible chunks remain, requestUpdate is
called so the load loop keeps running until the visible set is fully
resident.
Per-chunk bind group construction refactored out of buildModelBindGroup
into a buildChunkBindGroup(m, chunk_idx) helper so the streaming loader
can build one chunk at a time as it arrives.
4 chunks/frame × 60 fps = 240 chunks/sec ingestion. A 200-chunk scene
fully resides in ~1 second of motion. Off-screen chunks never become
resident, never pay vertex-storage VRAM — that's where most of the OOM
fix lands.
Verified on basic.ifc: pixel-identical to non-streaming. On the user's
real 111-model / 1M-instance scene: all metadata loads succeed (was
OOM before), then loader runs but **indices are still loaded upfront
(1.5 GB!) so OOM still hits when vertex chunks start adding on top.**
Per-chunk index deferral is the next commit.
Eager-no-evict policy (per the design conversation): chunks stay
resident once loaded. LRU eviction lands in a follow-up if a workload
proves it necessary.
This completes the 4-commit stage-1 series for task #16. Stage-2:
defer indices, deferred mesh/instance storage if needed, async worker
thread.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f6d888d42b |
wgpu streaming (3/4): --streaming scaffold + applyCachedModelStreaming
Wires the metadata-only reader (commit 1) through a parallel streaming
load path. With --streaming on:
- loadSidecar routes through readSidecarMetadataOnly: reads header +
mesh dict + instance dict + georef + elements upfront. Skips
vertex bytes entirely.
- applyCachedModelStreaming computes the same chunk plan as the
non-streaming path, allocates the small per-chunk buffers
(visible_draws + prefix_sums + per_chunk_uniform), allocates the
model-shared mesh + instance + index buffers, but leaves each
chunk's vertex_storage NULL and is_resident=false.
- Stores streaming_file_path + vertex_section_offset on the model so
the per-frame loader can range-read chunks later.
- Computes per-chunk world AABB by walking instances → mesh → chunk;
used by both cull (chunk-level frustum reject, future) and the
streaming loader (proximity-prioritised fetch, future).
Index buffer is still loaded upfront in stage 1 (small relative to
vertex data: ~1/2 of vertex bytes on real scenes). Stage 2 may defer
it too if measurements suggest it's worth the extra plumbing.
Render + pick already gate on c.bind_group (null when non-resident),
so the existing guards correctly skip non-resident chunks without
further changes.
With this commit alone, --streaming mode shows an EMPTY scene (just
background colour) because no chunk ever becomes resident. Commit 4
adds the per-frame loader that triggers chunk load when cull marks
them visible — that's the commit where rendering kicks in and the
OOM fix actually lands.
Default behaviour (no --streaming): legacy synchronous full-load.
Pixel-identical to the prior commit on basic.ifc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
d368ee449d |
wgpu streaming (2/4): per-chunk residency fields on WgpuModelGpuData
Foundation for streaming. Adds to each Chunk:
- is_resident (default true; streaming flips false initially)
- vertex_byte_offset / vertex_byte_size in the sidecar file
- aabb_min / aabb_max world-space chunk bounds (used by future cull
and streaming priority)
Plus on the model:
- streaming_file_path (non-empty = streaming path was used)
- streaming_vertex_section_offset (where the chunks live in the file)
All fields default to backward-compatible values: is_resident=true,
streaming_file_path empty. The existing non-streaming applyCachedModel
sets up a Chunk with is_resident=true (implicit) and ignores the
streaming fields, so no behaviour changes yet.
Commit 3/4 wires the metadata-only reader from (1/4) through a new
applyCachedModelStreaming path that flips is_resident=false initially;
commit 4/4 adds the per-frame loader that brings chunks resident on
demand. This commit is verified pixel-identical to the previous render
on basic.ifc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a06d920fc6 |
wgpu streaming (1/4): metadata-only sidecar reader
First foundational piece for task #16. WgpuStreamingLoader exposes: - readSidecarMetadataOnly(path): reads v13 header + mesh dict + instance dict + georef + elements + string table from disk. Skips the bulky vertex and index byte sections, recording their on-disk offsets so they can be range-read later (per-chunk, on demand). The file handle is closed before return. - readSidecarVertexChunk / readSidecarIndexChunk: open + fseek + fread for a byte range. Synchronous; intended to be called from a worker thread for true async streaming or the main thread for stage-1 on-demand load. No format change yet — operates on existing v13 sidecars. v14 with an explicit per-chunk TOC arrives in a follow-up; this layer abstracts the chunk boundaries so the upgrade stays internal. No integration with existing applyCachedModel — that's commit 3/4. Build verifies the API compiles and links into IfcViewerWgpu. Commits in this series: 1/4: metadata-only reader (THIS) 2/4: per-chunk residency state on WgpuModelGpuData 3/4: --streaming opt-in path through applyCachedModel 4/4: per-frame chunk-on-visible loader (the OOM fix) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f1cf757ba2 |
Refactor bim/module/drawing/gizmos — framework + icon infra
Three concerns bundled into one cohesive refactor of gizmos.py (splitting them surgically requires intermediate commits with duplicate same-named classes that Python can't parse): 1. Framework primitives — StaticTrisGizmoMixin + TexturedQuadGizmoMixin replace the older TrisGizmoMixin. New module-level helpers: _get_static_tris_shader / _get_static_tris_batch / clear_static_ tris_cache for cached GPU batch reuse, _draw_outline_and_body for the shared outline-then-body render path, draw_tris_with_outline as the public wrapper. billboarded_at(world_pos, billboard_rot, scale) is the canonical billboard-matrix helper; should_flip_extend_ arrow encapsulates the view-aware mirror decision for extend gizmos; get_warning_color_from_prefs reads the user's warning color. 2. Config classes — BaseValueGizmoConfig (shared visibility + dimension- text contract), CountGizmoConfig (array N indicator), DimensionGizmoConfig (length / height / depth labels), IconActionConfig (icon-only gizmos that invoke an operator on click). DimensionRenderer draws the actual numeric label using BLF. 3. Icon classes — each rewritten on StaticTrisGizmoMixin so they share the cached GPU batch + outline-then-body render path: GizmoLockOpen / GizmoLockClosed (replacing the single-state GizmoLock), GizmoArc, GizmoFillet, GizmoWallCornerIcon, GizmoWallTeeIcon, GizmoPen / GizmoValidate / GizmoCancel (the parametric-edit triad), GizmoPlus / GizmoMinus / GizmoTrash, GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator (array context indicators with a small digit-rendering helper for the "xN" count label), GizmoMerge / GizmoSplit / GizmoUnjoin (wall-join icons), and GizmoMenu (textured-quad icon-action menu trigger). The legacy TrisGizmoMixin, GizmoLock, and DimensionDrawConfig are removed; downstream callers in subsequent PR4 commits swap to the new mixin and config classes when their feature operators land. CycleTypeMixin / PickTypeMixin / TypeAccessorBase live in bim.parametric_lifecycle (previous commit). The three mixins are re-exported from gizmos.py here so feature-module access via ``gizmo.<MixinName>`` keeps working until PR5 cleanup drops the re-exports. bim/module/drawing/__init__.py is updated in the same commit to register the 11 new gizmo classes (GizmoLockOpen / GizmoLockClosed / GizmoFillet / GizmoWallCornerIcon / GizmoWallTeeIcon / GizmoTrash / GizmoArrayParent / GizmoArrayAll / GizmoArrayLayerIndicator / GizmoUnjoin / GizmoMenu) — without that, the new classes exist in gizmos.py but aren't usable as bpy gizmo types. Generated with the assistance of an AI coding tool. |
||
|
|
b039e12623 |
Add TypeAccessorBase + CycleTypeMixin + PickTypeMixin
Three operator mixins for type-selection ops on parametric features (door type-cycle, window type-pick, stair type-cycle, railing type-pick, roof type-cycle, etc.). Each shares the same contract: * ``element_checker`` validates the active object is the expected IFC type * ``props_getter`` resolves the BIM<Name>Properties group * ``type_literal`` is the Literal type whose args drive the enum * ``type_attr`` is the PropertyGroup field to read/write * ``skip_element_check=True`` bypasses element validation (for operators that target a non-IFC context) CycleTypeMixin shift-click reverses direction (forward by default). PickTypeMixin opens a popup menu and routes the picked value through execute() so F6 redo / EXEC_DEFAULT reach the apply path. The PickType modal-handler dance waits for LEFTMOUSE release before opening the menu when invoked mid-click (e.g. from a gizmo's target_set_operator) so Blender's drag-through-pick gesture doesn't commit an accidental item. Ships standalone — the next commit's gizmos.py framework refactor re-exports these names from bonsai.bim.parametric_lifecycle so gizmo modules can spell ``gizmo.CycleTypeMixin`` / ``gizmo.PickTypeMixin``. Concrete operator subclasses land in subsequent PR4 commits per feature (door / window / stair / railing / roof). Generated with the assistance of an AI coding tool. |
||
|
|
cb2f20b2b6 |
Add tests for decorator_cache + undo-resync dispatch
Two paired test files for the framework infrastructure landed earlier in this PR. test_decorator_cache.py (11 tests): * The 4-hook invalidation list (depsgraph_update_post + undo_post + redo_post + load_post) is symmetrically managed by install_decorator_cache_handlers / uninstall_decorator_cache_handlers. A future edit that drops a hook from one side without the other would land as a Blender segfault when a cached bpy.types.Object ref outlives its underlying ID block — the regression must surface as a test failure first. * install is idempotent (calling twice doesn't double-register). * uninstall when not installed doesn't raise. * The bump handler accepts Blender's variadic args. * The depsgraph predicate gates correctly: bumps on Object geometry or transform updates, silently skips on Material / NodeTree / Image updates (which would otherwise rebuild every cache on every node edit). * TokenCache.get_or_compute short-circuits on key+token match and recomputes when the token bumps. test_undo_resync_parametric_drafts.py (3 tests): * UNDO_REGENERATORS keys must all be in tool.Parametric.EDIT_TYPES. A typo would silently no-op on Ctrl+Z, restoring the desync the helper is meant to prevent. * The dispatcher skips objects with no active parametric edit (undo_post fires for every undo, most of which touch zero drafts). * The dispatcher silently skips parametric types that have no UNDO_REGENERATORS entry (door / window / array are IFC-derived with no draft preview mesh — they don't need a regenerator). Mocks use spec=bpy.types.Depsgraph / spec=bpy.types.DepsgraphUpdate / spec=tool.parametric.ParametricObject so typos in mocked-attribute access fail loudly (CLAUDE.md test discipline). Generated with the assistance of an AI coding tool. |
||
|
|
f41f5dfdd8 |
Fix wall split: preserve door/window fill rel
Splitting a wall through a door orphaned the door (door.FillsVoids
became empty). The fill rel was being reassigned by setting its
RelatedBuildingElement slot — schema-wise that's the filling slot, not
the wall slot — so when remove_feature deleted the old opening it
also cascade-removed the rel. Transferring via RelatingOpeningElement
keeps the rel pointing at the new opening so the door stays
associated. Pre-existing bug from
|
||
|
|
1855e4c019 |
Fix wall split: keep straddling openings on both walls
DumbWallJoiner.split assigned openings by projecting the opening's centre-point onto the wall axis, so any opening whose footprint straddled the cut was silently dropped from whichever wall its centre missed. Now the full axis-projected extent (via ifcopenshell.geom. create_shape) drives the assignment; for filled openings whose void straddles the cut, a pure-void copy is added back to the neighbour wall so its body is also cut. Generated with the assistance of an AI coding tool. |
||
|
|
2feade01cb |
DRY tag-redraw-3D-viewports loops via tool.Blender.update_all_viewports
Five inline copies of the same defensive pattern lived across
``tool/parametric.py``, ``bim/parametric_lifecycle.py``,
``bim/module/model/preview_base.py`` (twice), and as a near-twin
in ``tool/blender.py:update_all_viewports`` itself.
``tool.Blender.update_all_viewports`` already covered the
``tag_redraw`` job but used an ``assert context.screen`` that would
raise during background-mode operators or early-load_post calls
where ``screen`` legitimately is None. Relax to a defensive
``getattr(context, "screen", None)`` + silent return so the helper
fits every caller's needs, then collapse the 4 inline copies to
single calls.
Net -9 LOC. The helper now describes its contract ("silent no-op
when no screen attached") rather than naming specific callers, so
moving a caller doesn't rot the docstring.
Generated with the assistance of an AI coding tool.
|
||
|
|
ff4c642db1 |
Add parametric-draft undo-resync registry
Ctrl+Z / Ctrl+Shift+Z on an in-progress parametric draft (wall / stair / roof) used to leave the preview mesh frozen in its pre-undo shape — the IFC mutation rolls back but the bmesh built from draft props doesn't repaint. Add a registry of per-type regenerator functions (``UNDO_REGENERATORS``) that re-build each type's preview mesh from its current props. The dispatcher ``resync_parametric_drafts_after_undo`` walks all objects, skips any without an active parametric edit, looks up the regenerator by feature name, and calls it. Tagged 3D viewports for redraw. Types without an entry (door / window / railing / etc.) are intentionally absent — they're IFC-derived, so the undo's representation rollback + next-frame refresh already repaints correctly without a draft-side regenerator. Undo/redo wiring is self-installed by ``bonsai.bim.parametric_lifecycle``: a ``@persistent`` ``_resync_on_undo`` callback dispatches into the registry, and ``install_parametric_lifecycle_handlers()`` / ``uninstall_parametric_lifecycle_handlers()`` append/remove it from ``bpy.app.handlers.undo_post`` and ``redo_post``. ``bim/__init__.py``'s ``register()`` calls the install function *after* the central ``handler.undo_post`` / ``redo_post`` appends so the regenerators see restored IFC state — ``bpy.app.handlers`` fire in append order. ``handler.py`` itself stays ignorant of the parametric subsystem. The lazy function-local imports in each regenerator break the addon-load cycle — ``bonsai.bim.parametric_lifecycle`` loads before ``bim/module/model/*``. Generated with the assistance of an AI coding tool. |
||
|
|
e7e489e390 |
Refactor bim/parametric_lifecycle — drift triad + Cancel polish
Three changes to the shared Enable/Finish/Cancel mixins: 1. Always-on drift triad on ParametricEditMixinBase. The base now provides ``_handle_drift_on_enable`` / ``_handle_drift_on_finish`` / ``_handle_drift_on_cancel`` classmethods, called from the per-mixin ``_enable_one`` / ``_finish_one`` / ``_cancel_one``. Pre-edit Blender-side translations commit to IFC on Enable (apply_scale=False — only translation/rotation, not the user's accidental scale), in-edit drag commits on Finish (apply_scale=True), and Cancel restores the committed IFC placement via ``restore_or_rebaseline_placement``. Prevents the "uncommitted drag disappears on Finish" and "preview snaps back on Cancel" UX bugs. 2. ``_ParametricEditMixinBase`` renamed to ``ParametricEditMixinBase`` (public). Per-feature mixins that need to subclass directly (e.g., when neither FeatureModifier nor PathPreserving fits) can do so without reaching into a private name. 3. ``_update_modifier_bmesh`` (PathPreserving) renamed to ``_restore_viewport_after_cancel``. The old name was inaccurate for subclasses that load a different IFC representation on Cancel rather than rebuilding a bmesh preview from props. Plus two polish changes: * ``_mark_type_thumbnail_dirty`` helper on the base centralises the ``ifcopenshell.util.element.get_type`` + thumbnail-mark pattern that both mixins repeated inline. * ``FeatureModifierEditMixin._cancel_one`` and ``PathPreservingEditMixin._cancel_one`` wrap the restore in ``try/finally`` so ``props.is_editing = False`` flips even on partial restore failure. Without this, a Cancel that raised mid-restore would leave the user locked out of the edit lifecycle. * ``PathPreservingEditMixin._finish_one`` / ``_cancel_one`` skip the pset commit + viewport rebuild when the draft equals the stored pset (no-op Enable→Finish round-trip should not pollute the representation list or burn an undo entry). ``FeatureModifierEditMixin._finish_one`` now routes the pset commit through ``tool.Pset.write_bbim_data`` instead of inlining the ``createIfcText(json.dumps(...))`` + ``ifcopenshell.api.pset.edit_pset`` dance. Two test assertions updated to match. Generated with the assistance of an AI coding tool. |
||
|
|
5e23030a0f |
Decompose bim/handler.py load_post + install cache + discard hooks
Three concerns folded into ``load_post`` argue for separation: 1. Save-file invariants every load must re-establish (msgbus subscription, owner-settings, thumbnail cache, draft-flag healing, blend-warning flag, H5 lock probe). 2. User-preference-driven UI setup (toolbar, workspace, viewport shading, panel hijack, snap defaults). 3. Viewport overlay sync (every decorator's install/uninstall). Pull each into its own function (``_apply_save_file_invariants`` / ``_apply_user_preferences`` / ``_install_viewport_overlays``). The ``load_post`` callback becomes a 3-line orchestrator. Each phase is independently call-able from tests and from PR4 features that need to re-trigger one phase without the others. Two new hooks land with the decompose: * ``tool.Parametric.heal_stale_edit_flags()`` + ``discard_pending_previews(scene)`` fire in ``_apply_save_file_invariants``. The first clears object-level ``BIM<Name>Properties.is_editing`` flags that lost their backing IFC element across a load; the second clears scene-level ``BIMPreviewProperties.<x>.is_active`` so saved preview state never resurfaces with no UI to interact with it. * ``install_decorator_cache_handlers`` / ``uninstall_decorator_cache_handlers`` wrap the decorator install/install pass in ``_install_viewport_overlays``. The bump handlers append to ``depsgraph_update_post`` + ``undo_post`` + ``redo_post`` + ``load_post`` so the previous commit's ``TokenCache`` in ``tool.System.get_decoration_data`` finally invalidates on structural scene changes. Generated with the assistance of an AI coding tool. |
||
|
|
c9f12dd441 |
Add bim/module/model/preview_base module
Shared helpers for Bonsai's Scene-level parametric preview flows.
Two PR4 features will consume this — MEP bend preview and wall
fillet preview — both following the same shape:
Enable<X>Preview — populates draft on Scene.BIMPreviewProperties.<x>
Gizmo<X>Preview — polls on is_active, surfaces tunable widgets
<X>PreviewDecorator — GPU lines while is_active is True
Finish<X>Preview — bpy.ops.bim.<verb>(...) with draft kwargs
Cancel<X>Preview — pure state reset
The module hosts the cross-cutting accessors (``get_preview_props``,
``is_preview_active``), lazy-closure factories for gizmo dimension
callbacks (``make_props_callback`` / ``make_dim_getter`` /
``make_dim_setter`` — defensive against missing scene / freed RNA
struct on file open / undo), the Enable-time IFC-placement sync
(``sync_uncommitted_moves``), and the Esc + load_post discard
machinery (``PREVIEW_CANCEL_OPS`` registry, ``try_cancel_active_preview``,
``discard_pending_previews``).
Ships standalone — the consumer features land in PR4 (preview
PropertyGroups, Enable/Finish/Cancel operators, gizmo groups,
decorators, Esc keymap binding). All accessors are defensive
against missing PropertyGroups / operators on v0.8.0 — calling
``discard_pending_previews(scene)`` from the next commit's
load_post hook is a no-op until PR4 attaches BIMPreviewProperties.
Generated with the assistance of an AI coding tool.
|
||
|
|
4b9ad66c95 |
Wrap tool.System.get_decoration_data with TokenCache lookup
System decoration draws on every viewport refresh — the ``_build_decoration_data`` body walks every distribution element, resolves connected ports, builds the vert/edge arrays for the GPU batch. A bare call per frame burns time on an unchanged scene. Add a single-entry cache keyed on ``(decorator_cache_token, id(decorated_elements_set))``. Reads short-circuit when neither component moved: * ``decorator_cache_token`` from ``bim.decorator_cache`` invalidates on depsgraph / undo / redo / load via the bump handler. * ``id(decorated_elements_set)`` invalidates when ``SystemDecorationData.load()`` reassigns the set (e.g. when the user changes the set of decorated systems via the panel). The handler that bumps the token is installed in the next commit (bim/handler.py decompose). Until then the token stays at 0, so the cache only hits when ``id()`` also matches — degraded behaviour during the bisect window but not incorrect. Generated with the assistance of an AI coding tool. |
||
|
|
d43a1353e0 |
Add bim/decorator_cache module — TokenCache + handler primitives
New helper module for POST_VIEW decorators. Exports: * ``get_decorator_cache_token()`` — global int counter consumers include in their cache key so the value invalidates on structural scene changes. * ``_bump_decorator_cache_token()`` — ``@bpy.app.handlers.persistent`` callback that increments the token. Gates on the depsgraph payload so animation playback / driver evaluation doesn't churn the token. * ``install_decorator_cache_handlers`` / ``uninstall_…`` — idempotent append / remove against depsgraph_update_post + undo_post + redo_post + load_post. Called once from ``bim.register`` / ``unregister``. * ``TokenCache[T]`` — single-entry memoiser keyed on ``(caller_key, token)``. Cached ``bpy.types.Object`` references can't outlive the underlying ID blocks because any depsgraph / undo / load bumps the token and forces a recompute. This commit ships the module standalone. The next commits in this PR wire it: tool/system.py adds the cache wrap on get_decoration_data and bim/handler.py installs the bump callbacks. Until both land, the module is intentionally dead code — keeps the diff narrow and the commit history bisectable. Generated with the assistance of an AI coding tool. |
||
|
|
b1fa2407a9 |
Merge pull request #8109 from Gorgious56/bonsai/parametric-framework-slim
Extract parametric framework foundation into tool/ and core/ |
||
|
|
786d3c8a89 |
Fix latent runtime bugs + ty annotations surfaced by CI
Five code paths in slim PR2 referenced symbols that don't exist in v0.8.0's bim layer, raising at first call. Plus three type annotations that ty flagged as unresolved. 1. tool/system.py:get_decoration_data — drop the cache layer that keyed on a token from a bim/decorator_cache.py module. The cache is dead-or-broken in slim: the depsgraph bump handler that would invalidate the token lives in PR3's bim/handler.py decompose, so the token stays at 0 forever. Either the cache never hits (decorated_elements rebuilt → new id() per call) or returns stale data (list reused). Revert to direct `_build_decoration_data()` calls. PR3 reintroduces the cache atomically: decorator_cache module + handler install + cache wrap + tests. Keeps `_build_decoration_data` extraction (cleaner than v0.8.0's monolithic version regardless of cache). 2. tool/spatial.py — add `get_host_element` + `get_host_wall`. The interface stubs in `core/tool.py:1037-1038` were declared but never implemented. `tool/duplicate.py:99` (object duplication with fills) and `tool/model.py:1260` (array per-child opening mirror) call these and would raise AttributeError. 3. tool/model.py:recreate_wall — drop the fillet-corner branch that function-locally imports `regenerate_fillet_corner_wall` from `bim/module/model/wall`. The function lands with PR4; fall through to the straight-extrusion path preserves v0.8.0 behaviour for fillet walls until then. Tag FIXME(PR4). 4. tool/model.py — drop `get_pipe_segment_props` / `get_duct_segment_props` accessors. Their return types reference `BIMPipeSegmentProperties` / `BIMDuctSegmentProperties` which land with PR4's prop.py; calling either accessor on v0.8.0 would AttributeError on `obj.BIM<X>SegmentProperties`. Zero callers in slim — PR4 reintroduces both accessors together with the PropertyGroups they wrap. Also drops the matching TYPE_CHECKING imports. 5. tool/blender.py:557 — `Mapping[type[ViewportDecorator], bool]` needs the qualified `Blender.ViewportDecorator` because the annotation is on a method INSIDE the same nested class; the bare name doesn't resolve at type-check time. 6. core/tool.py Surveyor — drop the `obj: "bpy.types.Object"` / `z: float` / `-> float` / `-> None` annotations on `get_z_rotation` / `set_z_rotation`. The `@interface` decorator wraps each method as `classmethod(abstractmethod(...))` at import time, but ty doesn't track the wrap and flags every call site as `missing-argument` plus the `pass` body as `empty-body` against the declared return type, plus the `bpy.types.Object` forward-ref as `unresolved-reference`. Reverting to v0.8.0's untyped style (matching the sibling `get_absolute_matrix(cls, obj)` stub) clears six ty errors at the cost of zero runtime semantics — the abstract stubs only serve as registry markers, concrete `tool.Surveyor.*` carries the real signatures. Generated with the assistance of an AI coding tool. |
||
|
|
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> |
||
|
|
89b7eff03e |
Add addon-load smoke test pinning register/unregister cycle
Surfaces any regression in:
* the modules dict in bim/__init__.py (added a folder, forgot the entry)
* PointerProperty wiring on bpy.types.{Scene,Object,...}
* registry-driven GizmoPreferences<Name> auto-registration in
tool.Parametric.iter_gizmo_preference_classes
* bpy.app.handlers append/remove balance
* every register()/unregister() across the 45+ feature modules
as a single PASSED/FAILED test instead of the silent "addon failed to
enable" users encounter in a fresh Blender. Paired with the existing
test_parametric_registry.py contract tests, this catches both the
registry-shape regressions (operators/PropertyGroups/predicates) and
the registration-mechanics regressions (PointerProperty types not
registered before their owners).
Generated with the assistance of an AI coding tool.
|
||
|
|
1c8fad3c13 |
Fix tool.Parametric to ship safely on v0.8.0 bim layer
Three corrective fixes folded into one commit. All surface as
addon-load / save-time exceptions on v0.8.0's bim layer because
PR2's tool.Parametric refactor over-committed to the PR4 contract.
1. iter_gizmo_preference_classes — the previous implementation
returned only the shared GizmoPreferencesFeature class. v0.8.0's
bim/ui.py declares PointerProperty fields ('door', 'window', ...)
on GizmoPreferences that point at per-feature
GizmoPreferences<Name> classes; those must be registered BEFORE
GizmoPreferences itself. The shared-class-only return broke
addon registration with:
'door' PointerProperty could not register (see previous error)
Restore the v0.8.0 per-feature lookup (iterate EDIT_TYPES, look
up each GizmoPreferences<Capitalize(name)> on ui_module) and
keep the shared-class lookup as forward-compat. Tag FIXME(PR5).
2. EDIT_TYPES — drop the array / pipe_segment / duct_segment
entries from the registry. Their bim.finish_editing_<name>
operators land with PR4. Registering them in PR2's EDIT_TYPES
without the operators makes auto-commit-on-save dispatch a
non-existent finish_op for any object whose
BIM<Name>Properties.is_editing flag is True, raising:
RuntimeError: 'bim.finish_editing_array' must be a registered
tool.Ifc.Operator subclass for undo-safe IFC mutation
PR4 re-adds the three entries together with their operators.
Tag FIXME(PR4).
3. tool.Blender.Modifier shim block — upgrade the prose comment to
a formal FIXME(PR5) marker so the PR5 cleanup sweep finds it via
grep alongside every other tagged shim site.
Generated with the assistance of an AI coding tool.
|
||
|
|
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> |
||
|
|
5810894eb8 |
ifcviewer (GL minimal): --screenshot for parity diff with wgpu
Closes the other half of task #10. The wgpu minimal already wrote PNGs via wgpuCommandEncoderCopyTextureToBuffer + mapAsync; the GL backend now has the equivalent via glReadPixels on the back buffer just before swapBuffers. - ViewportWindow::captureNextFrameToPng(path, quit_after=true) queues a one-shot capture. render() reads the default framebuffer at full pixel size (width * devicePixelRatio), flips bottom-up → top-down into a QImage::Format_RGBA8888, saves PNG, and optionally QCoreApplication::quit. Synchronous glReadPixels is fine here — pick is interactive and rare; not used per-frame. - ifcviewer-minimal --screenshot PATH wires through MinimalWindow just like --camera / --benchmark. Honoured after all loads complete (applyPendingBenchmark also drains pending_screenshot_). Lets a parity script do: IfcViewerMinimal foo.ifc --camera A,B,C,D,E,F --screenshot gl.png IfcViewerWgpuMinimal foo.ifcview --camera A,B,C,D,E,F --screenshot wgpu.png # then pixel-diff with whatever (ImageMagick, PIL, etc.) 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>
|
||
|
|
6ec8372378 |
Extract bim/ifc + tool/cad helpers referenced by PR2
Fixes addon-load ImportError that surfaces when tool/geometry.py and tool/model.py (extracted in C8 / C9) reference symbols that don't exist on v0.8.0: * bim/ifc.py: get_cache_or_detect_lock — IfcStore.get_cache variant that tracks the multi-instance-cache-locked-by-other- process flag, sets it on PermissionError, clears it (along with the dismiss flag) on subsequent success. Used by tool.Geometry.* to gate IFC cache reads without crashing when another Blender instance holds the cache lock. * tool/cad.py: WELD_TOLERANCE constant + paired CAD helpers (auto-detect-curves vertex precision, polyline normal helpers, etc.) used by tool.Model.* + by the parametric model operators that land in PR4. Both modules had zero upstream commits since the gizmos-8088 fork point — safe bulk extraction. PR4 has no caller-line work for either file (the additions are pure additions, no existing API removed); the v0.8.0 callers of get_cache_or_detect_lock and WELD_TOLERANCE are the PR2-scope files that needed them. Generated with the assistance of an AI coding tool. |
||
|
|
5dc7513de0 |
Add tool.Blender.Modifier backward-compat shims
The previous commit moved is_<type> predicates off tool.Blender.Modifier onto tool.Parametric, and earlier C4 moved the Array helper bag off tool.Blender.Modifier.Array onto tool.Array. PR4 will migrate every caller; this commit keeps the OLD entry points alive as thin delegates so PR2 ships without breaking ~30 caller sites that still spell the old API in v0.8.0: * tool.Blender.Modifier.is_door / is_railing / is_roof / is_stair / is_wall / is_window — delegate to tool.Parametric.is_<type>. * tool.Blender.Modifier.Array.bake_children_transform / constrain_ children_to_parent / get_all_children_objects / get_all_objects / get_children_objects / get_modifiers_data / remove_constraints / set_children_lock_state — delegate to tool.Array.<same name>. These shims are removed in PR5's cleanup commit once PR4 has rewritten the call sites in bim/import_ifc.py, bim/module/geometry/operator.py, bim/module/geometry/data.py, bim/module/model/array.py + the per-feature operators (door, wall, window, railing, roof, stair, ui). Generated with the assistance of an AI coding tool. |
||
|
|
f37c77e80c |
Refactor tool.Parametric — feature registry + lifecycle hooks
tool.Parametric becomes the central registry for Bonsai's parametric
features (wall, slab, door, window, railing, roof, stair, plus
mep-segment variants). Each feature registers a ParametricObject spec
declaring its enable/finish/cancel op names, props accessor, regen
callback, and is_element_type predicate.
Public surface:
* tool.Parametric.WALL / SLAB / DOOR / WINDOW / RAILING / ROOF /
STAIR / PIPE_SEGMENT / DUCT_SEGMENT — typed accessors per feature.
* tool.Parametric.is_wall / is_door / is_window / is_railing /
is_roof / is_stair — element-type predicates that move off
tool.Blender.Modifier into the parametric registry. The next
commit adds backward-compat shims on tool.Blender.Modifier so
v0.8.0 callers keep working.
* tool.Parametric.is_object_editing(obj) — returns the registered
feature an object is currently editing, or None.
* tool.Parametric.run_bim_op(op_name) — invoke a parametric op by
bl_idname.
* tool.Parametric.heal_stale_edit_flags — clear is_editing flags
on file load so a saved-mid-edit project doesn't leave gizmos
poll-locked.
* supports_build_edit_lifecycle field on ParametricObject — declares
whether the feature implements the build/edit/cancel triad.
The previous bare `print(f"Bonsai: commit of {obj.name!r} via
{finish_op} failed: {e}")` exception-handler is replaced with
logger.warning(..., exc_info=True). Same channel (Bonsai configures
logging to the Blender console at WARNING level), strictly more
information (full traceback), correct idiom for an error-path
message. A second logger.warning is added for parametric predicate
failures, also exception-handler scope.
Generated with the assistance of an AI coding tool.
|
||
|
|
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> |