Commit Graph
142 Commits
Author SHA1 Message Date
Karim Jerbi 7aafd9c710 refactor(selection): split 31-field SelectionState into five sub-structs
SelectionState now holds view, gesture, input, menu and orbit sub-structs; all 450 access sites updated mechanically with no behavior change. Deletes three write-only dead fields (box_last, box_last_crossing, poly_last_crossing). Hand-rolled clears in commands and dim_viewport route through the new clear methods; bench fixture updated. Full suite: 1976 passed / 0 failed / 25 ignored.
2026-10-06 20:22:04 +01:00
Hakan SevenandClaude Opus 5.5 3dcebf0dc6 Merge pull request #1553 from ramox81/codex/pointcloud-attach
Point clouds: drawn from their scans, attached, cropped and stylized.

The fork's codec patch is resolved to the upstream pins: the point cloud
records landed in opencadcodec 5ef9376, carried through opencadkernel
68eeda1 and opencadgraph e204966.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:49:25 +03:00
Hakan SevenandClaude Opus 5.5 2a15a370f6 fix(dimension): an edited dimension keeps its layer's linetype
A dimension loaded from a file draws from its baked *D block, whose
lines are ByBlock and so take the dimension's linetype (ByLayer: the
layer's). Any edit - text colour, MATCHPROP, a grip - drops that block
and the dimension draws live, where a style with no DIMLTYPE/DIMLTEX1/2
drew solid: an edited dimension on a dashed layer turned solid (#898).
None or ByBlock now takes the dimension's own linetype, ByLayer its
layer's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:39:33 +03:00
ramox81andClaude Opus 5.5 15c0da8b79 Point clouds: measured lighting, highlighted extents box, gizmo shaft grab
- Lighting follows the view: the default source is a key light from the
  upper left and a fill light from the lower right over an ambient term,
  the other source one light from above; POINTCLOUDLIGHTING 1 lights the
  front faces, 2 both.
- A selected or hovered point cloud shows its extents box.
- A move-gizmo arrow can be grabbed along its shaft; the grab offset is
  taken on the press, so click-and-place and typed distances start from
  the insertion point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:21:33 +03:00
ramox81andClaude Opus 5.5 b69ae291bb Point clouds: lighting, 2D visual style, real-time density
Points carry their world normal: POINTCLOUDLIGHTING lights them in the
shaded visual styles (1 lit from both sides, 2 with back faces dark),
from the view (POINTCLOUDLIGHTSOURCE 0) or the default lights (1).
The 2D wireframe style draws no cloud points: a found cloud shows its
extents and "Point clouds are not displayed in 2D visual style.",
or nothing with POINTCLOUD2DVSDISPLAY 1. While the view moves only
POINTCLOUDRTDENSITY per cent of the points are drawn; the points are
stored in an order where every leading part samples the whole cloud.
The colour map dialog lists its schemes by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:54:03 +03:00
ramox81andClaude Opus 5.5 e9c4e501db Point clouds: drawn from their scans, placed, cropped and attached as the reference does
Scans (.rcs) and projects (.rcp) are read and their points drawn as
screen-sized dots, coloured by the cloud's stylization. The cloud's
placement carries its scale and rotation; its extents and crops stay in
the scan's coordinates. A missing scan shows the extents with the saved
path. -POINTCLOUDATTACH / POINTCLOUDATTACH, POINTCLOUDCROP and the
POINTCLOUD* settings follow the reference's prompts and messages, the
Properties list is the reference's, and a locked cloud is left alone by
edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:08:46 +03:00
Hakan SevenandClaude Opus 5.5 60f35e2b1d fix(color): only colour 7 swaps with the background; true colours draw as authored
Every near-white colour turned black on a light background (and near-black
white on a dark one), including true colours. A 255,255,255 solid used as a
white mask became a black block. Now only colour 7 swaps: true colours and
ACI 255 resolve a hair off pure white/black and keep their value on screen
and in plots.

Discussion #1500

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:16:18 +03:00
Hakan SevenandClaude Opus 5.5 fdc01e13c8 fix(render): show Inventor drawing views, each in its own viewport (#1285)
Inventor DWG exports place every sheet block with AcIdBlockReference,
which the codec skipped, so these drawings opened blank. opencadcodec
7c20aa7 decodes it as an INSERT and keeps the viewport each model-space
view reference is bound to.

Those view blocks overlap in model space, so a content viewport now also
hides model-space references bound to a different viewport. The handles
join the viewport's frozen-layer set (entity and layer handles never
collide), which the wire and render-graph visibility checks and every
per-viewport cache key already use.

Pins opencadcodec 7c20aa7, opencadkernel d71a828, opencadgraph 1543a5a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:48:36 +03:00
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:34:23 +03:00
Karim Jerbi 3efd62b6ef perf(scene): optimize render environment scan and avoid insert clone in style spec
Eliminate full Insert entity cloning in block style evaluation, unify
document object environment scans, and guard viewport override queries.

1. Analysis & Architectural Bottleneck:
   - In render_graph::InsertStyleSpec::new, an entire Insert struct was cloned
     into an EntityType::Insert enum solely to pass to style functions that only
     read EntityCommon fields (layer, color, transparency, linetype, lineweight).
   - In view::render::resolve_document_render_environment, when object caches were
     not prepared, the document.objects map was traversed linearly twice: once
     for RenderEnvironment and once for RenderSettings.
   - Viewport override lookups were being performed even when viewport was None
     (the standard model space condition).

2. Optimizations Implemented:
   - Added `render_style_for_common_viewport`, `linetype_name_for_common_viewport`,
     and `has_resolved_book_color_common` taking `&EntityCommon` directly.
   - Constructed `InsertStyleSpec::new` without cloning the Insert entity.
   - Fused the two linear scans of `self.document.objects` in
     `resolve_document_render_environment` into a single pass tracking best candidates.
   - Guarded viewport override lookups with `if viewport.is_some()` in
     `layer_render_style_viewport` and `render_style_for_common_viewport`.

3. Correctness & Invariance:
   - The same fields and fallback values are resolved in identical order.
   - Tested across all 325 scene tests, block inheritance tests, and viewport
     override unit tests with 0 failures.

4. Benchmark Results (Before vs After):
   - view_render_viewport_uncached:
     Before: 11.00 µs (90.9k FPS)
     After:   5.00 µs (200.0k FPS)
     Improvement: -54.5% latency (2.2x faster uncached rendering)
   - wide_and_tapered_arc_tessellation:
     Before: 6.36 ms (235.8k entities/s)
     After:  5.41 ms (277.4k entities/s)
     Improvement: -14.9% latency (+17.6% throughput)
   - analytical_curved_tessellation:
     Before: 7.79 ms (359.5k entities/s)
     After:  7.01 ms (399.6k entities/s)
     Improvement: -10.1% latency (+11.1% throughput)
   - spatial_interaction_index_build:
     Before: 0.77 ms (2.47M wires/s)
     After:  0.73 ms (2.66M wires/s)
     Improvement: -5.1% latency (+7.7% throughput)
   - ui_grip_budget_build:
     Before: 424.50 µs
     After:  380.25 µs
     Improvement: -10.4% latency
   - ui_ribbon_view_construction:
     Before: 455.50 µs
     After:  382.30 µs
     Improvement: -16.1% latency
2026-09-24 09:05:08 +01:00
Hakan Seven 5719fcc86f Merge pull request #1430 from yztink:fix/viewcube-visibility 2026-09-22 12:28:01 +03:00
Karim Jerbi 874dd9debf perf(scene): optimize view render environment resolution and wire splitting
Eliminate major performance bottlenecks in the per-frame view render path:
- Memoize document render environment resolution using (geometry_epoch, len) keying, replacing per-viewport O(N) linear scans over document.objects.
- Index render_environments and render_settings handles in ObjectDataCache during the single build_cache pass.
- Cache resolved background image lookups in memory to eliminate repeated path.is_file() filesystem syscalls per frame.
- Add has_face3d fast-path to bypass wire splitting when no Face3D entities exist, and use FxHashSet<u64> handle indexing when present.
- Invalidate all cached structures across bump_geometry, bump_geometry_no_blocks, and bump_entities.
- Add comprehensive correctness unit tests and before/after benchmarks.

Performance impact (10k objects):
- P95 latency reduced from 341.00 µs to 173.00 µs (-49.3%)
- Throughput increased from 4,700 FPS to 6,000 FPS (+27.7%)
- Direct render environment evaluation: ~85 µs down to <50 ns (>1,700x)
2026-09-22 09:27:47 +01:00
yztink dad9c725c2 fix(viewcube): restore ViewCube visibility across display scales and window bounds 2026-09-21 23:39:48 -07:00
huanld 36d5c4db79 fix(render): compose PLOTSTYLE wire-fill depth like the wire pipeline
When a display plot style maps an ACI to a fill pattern, wire fill
triangles are re-emitted as PLOTSTYLE hatches. Those took
depth_override raw — a per-block child label for block wires and 0.0
for top-level ones — while everything else in the hatch/wipeout pass
sorts at the composed depths[insert] + label * half. The fill could
land outside its host's band and get buried under its own block's
masks (same shape as the block-text bug).

Both the display and PDF paths now travel through wire_draw_depth /
the PlotWire's composed depth, and the display cache key rides the
depth generation so a DRAWORDER edit re-ranks the fills without
needing a wire rebuild.
2026-09-18 08:38:35 +07:00
Hakan Seven 08bb17b0b8 Fix constraint editing and glyph feedback 2026-09-15 21:55:40 +03:00
Hakan Seven c9eeb6f66e Merge pull request #1284 from ramox81/codex/autoconstrain-parity
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/app/view/mod.rs
#	src/ui/overlay.rs
2026-09-15 12:38:41 +03:00
ramox81 66be530655 Complete automatic constraint behavior 2026-09-15 11:47:28 +03:00
Christopher Overbeck 37cfa06b02 Preserve fractional viewport placement when compositing paper space
Keep fractional raster placement and matching integer texture coverage
through compositing. Apply parent clipping without changing the scale.

Fixes #1267
2026-09-14 17:07:57 -05:00
ramox81 6272722baa Carry hatch origin through creation and pattern editing 2026-09-11 13:40:40 +03:00
Hakan Seven 4972bbed70 Fix issues found while integrating performance PRs 2026-09-08 17:33:02 +03:00
Hakan Seven 2b510c1a1e Merge pull request #1076 from mechpanos/pr/skip-partition-on-patch 2026-09-08 17:17:59 +03:00
PanosandClaude Opus 5 eef007d080 perf: report a skipped partition without printing zero counts
The skip branch printed `circles=0 ellipses=0`, which reads as "there are
no circles" when it means "not recomputed this frame" — and those counts
were the evidence that confirmed the skip works, so the wording nearly
cost a misreading of it.

The skip now prints its own line with what it retained.

Measured effect of the skip itself, on the reproducer: a drawn point goes
from 98 ms to 17-39 ms, `prepare` from 89 ms to 11-23 ms. Frames that draw
a circle still re-partition, and the instance counts show it — 40868,
40869 … 40874, one per circle, with the line frames skipped in between.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:42:39 +03:00
PanosandClaude Opus 5 03c64e3d38 perf: skip the wire partition when a patch cannot change its output
`arena-post partition=55.2ms blocks=5.9ms curves=2.5ms` — the partition is
almost the whole of the 73 ms an arena patch spends after patching, and it
walks all 236 956 resident wires to serve a one-entity change.

Its three consumers here — the instanced, circle and ellipse uploads —
depend only on wires that are block references or analytical curves. A
patch that touches none of those cannot change any of them, so the walk is
skipped and the uploads already on the slot are kept.

Two things make that sound, and both are checked rather than assumed:

**Depths hold still.** The instances bake a draw depth.
`inserted_draw_depth_label` allocates into a reserved gap instead of
renumbering, so adding an entity leaves every other label alone — the
existing doc comment says so and there is now a test. A rebuild from
scratch *can* move them, so `draw_depth_generation` bumps only there, and
a slot reuses its uploads only while that generation is unchanged.

**Contributors are remembered.** `partition_wires` records which handles
fed those three outputs, and the skip requires that no changed handle is
among them and that no changed handle's new wires would join them. That
catches an entity entering the set and an entity leaving it. The
membership test is shared with the walk it decides to skip, so the two
cannot drift, and it is deliberately conservative: a wire that looks
analytical but fails extraction forces a needless re-partition, never a
stale upload.

`arena-post` gains `skipped=` so a run says how often this actually fires
rather than leaving it to be inferred from the timing.

Not verified on a device yet — the headless harness never reaches
`prepare`, so this needs a GUI run on the reproducer to confirm both the
saving and that nothing goes missing on screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:42:38 +03:00
PanosandClaude Opus 5 feb3ca4d28 perf: split the arena patch's post-work into its three steps
With the selection-filter rebuild gone, a drawn point costs 98 ms and
`prepare` is 93 % of it:

    wire  73.0ms  arena-patch  wires=236961 gpu_instances=405551
    wire-cull      5.1ms
    face3d-upload  3.9ms
    text-upload    3.0ms
    resident-patch 1.0ms  changes=1
    draw-depth-patch 0.0ms

`arena-base ok=true patch=Some(16) changes=1` — the arena patches
incrementally, one entity, and still takes 73 ms. The patch itself is
careful; the three steps that follow it are not, and all three are
O(resident set) rather than O(changes):

    partition_wires(&vp_wires, …)          236 961 wires, every frame
    upload_block_wires(…instanced…)         10 719 placements rebuilt
    upload_circles/ellipses_from_instances

`arena-post partition= blocks= curves=` says which dominates. Guessing
between three plausible candidates is what the earlier rounds of this work
paid for repeatedly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:42:38 +03:00
PanosandClaude Opus 5 d267083a29 perf: say which message caused an edit's latency
The first measurement showed four consecutive edits at ~192 ms each with
a handler cost of 0.55 ms. That is below the `update` line's 5 ms
threshold, so the log reports the latency but not what asked for it —
and whether a live command preview is bumping the geometry epoch on every
mouse move is exactly the question the numbers raise.

`nav-prepare` now carries the message label alongside the op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:42:21 +03:00
Karim Jerbi dd061a55ed perf(render): single-pass viewport wire classification and partitioning
- Add fast classify_wire to bypass circle/ellipse extraction for the 95%+ of wires that are straight/polylines.
- Implement single-pass partition_wires extracting regular wires, mesh edges, block wires, circles, and ellipses in one traversal.
- Upload circle and ellipse instances directly from pre-extracted buffers in PersistentWireArena and build_wire_buffers without redundant iteration.
2026-09-07 13:55:16 +01:00
Karim Jerbi 0425f979f9 feat(render): GPU analytical rendering for circles, arcs, and ellipses
- Render planar circles, circular arcs, ellipses, and elliptical arcs analytically via GPU-instanced screen-space quads and WGSL shaders.
- Implement exact arc sweep clamping with round end caps matching the CAD wire shader.
- Implement robust Newton-Raphson closest-point solver for ellipses with isotropic screen-space derivative anti-aliasing.
- Integrate into wire pipeline and PersistentWireArena upload paths.
- Remove unused mesh_volume_centroid function in doc_api.
2026-09-07 13:50:12 +01:00
Hakan Seven ac8a545669 Fix layout cache correctness and GPU resource recovery 2026-09-05 19:56:49 +03:00
Hakan Seven 1a4b389b0e Merge pull request #1014: perf: let one tessellation serve every background 2026-09-05 19:28:34 +03:00
PanosandClaude Opus 5 391a74a615 perf: let one tessellation serve every background
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.

`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.

`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.

The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.

The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.

`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.

Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.

`wires-build` gains `colors=` so the pass has a number of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:16 +03:00
PanosandClaude Opus 5 49318a1c55 perf: allocate a shadow map only for viewports that cast shadows
`gpu-live` made this the obvious next one. Once idle slots started giving
their memory back, the shadow maps were the largest thing that never did:
128 MiB across eight slots, 37 % of the 341 MiB floor, and none of it
sampled — `shadow_enabled` defaults to false and most visual styles never
turn it on.

Every slot allocated `SHADOW_MAP_SIZE` squared at `Depth32Float` in
`Pipeline::new` and held it for the life of the slot, because the frame
bind group needs *something* at its shadow binding and the field was not
optional.

It is now `Option<(Texture, TextureView)>` alongside a 1x1 placeholder
that satisfies the binding while shadows are off — nothing samples it
then, so one texel is the whole requirement. `sync_shadow_target`
allocates on the transition into shadows and drops on the way out, and
only on the transition, so a viewport that keeps its setting does not
rebuild a bind group every frame.

The frame bind group had two inline builders — one in `Pipeline::new`, one
in `upload_background_images` — and this adds a third caller, so they are
now one `rebuild_frame_bind_group`. A shadow transition cannot forget the
background image the slot already had.

The shadow pass now tests for the target rather than the flag: wanting
shadows and having somewhere to put them are different questions, and
without the second one a failed 16 MiB allocation would have rendered the
pass into the 1x1 placeholder. Releasing a cold slot hands the target back
too, and `gpu-live` reports what is actually held rather than a constant.

Covered by a GPU test that toggles the setting off/on/off/on against a
real adapter with a validation scope armed, then releases the slot — the
transition is the risky path and the other GPU tests never take it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:09 +03:00
PanosandClaude Opus 5 deebc14012 fix: a rejected upload must not latch the slot as up to date
The drawing vanished from Model and only came back with REGENALL. That is
this bug's exact signature, and the guard added two commits ago was
incomplete.

`cached_wire_id` gates the entire wire upload block — line 620 skips it
whenever the slot's id already matches the viewport's content id — and it
was stamped unconditionally at the end of that block, whether or not the
device had accepted what was uploaded. So a rejected allocation left the
slot holding invalid buffers *and* claiming to be current. Nothing
rebuilt it. REGENALL worked because it mints a new content id, which is
the one thing that makes the ids differ again.

`wire_buffer_cache` and `wire_arena_id` were guarded; this third latch,
the one that gates them both, was missed. Guarding it individually would
leave the same question for `cached_text_source`, `cached_hatch_source`
and the rest, each stamped after its own upload.

So the check moves to where it covers all of them: if the device reported
anything during this viewport's turn, the slot forgets every cache key it
holds and the next frame rebuilds it. The counter is device-wide, so an
error raised by one viewport also invalidates its neighbours in that
frame. That is deliberate — the cost is one slow frame, and the
alternative is the failure this fixes.

The narrower guards stay: `wire_buffer_cache` is shared across slots, so
keeping a bad entry out of it protects viewports that were healthy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:09 +03:00
PanosandClaude Opus 5 0118b28d10 fix: give back the memory of viewports nobody is drawing
`MultiPipeline::inners` only ever grew. A slot that no viewport is using
still owned its wire arena, its render targets, its text atlas and every
category buffer — tens of megabytes each — and nothing ever asked for them
back. Zoom a paper layout out until five viewports are on screen, and the
memory the hidden ones are holding is exactly what the visible one cannot
get.

`Pipeline::release_heavy_resources` hands it back: arenas, wire and block
batches, the handle index, selection and preview buffers, wipeouts,
images, mesh batches, silhouettes, the text atlas, and the render targets,
which go through `ensure_depth_texture` at its smallest size rather than
being reached for directly so the blit bind group stays consistent with
the views it samples — about 0.6 MiB against the ~70 MiB a full-canvas
slot holds.

Everything released is derived state whose rebuild is guarded by a cache
key, and every one of those keys is reset. That reset was already written
once, inline, for slot reuse; both now call `forget_cached_keys` so they
cannot disagree about what "this slot holds nothing you can trust" means.
It gained two entries in the move — `cached_highlight_key` and
`silhouette_source_key` — which slot reuse had not been clearing, so a
recycled slot could keep a stale highlight or silhouette.

The policy is gradual while the device is healthy: a slot is released once
it has been idle for 64 prepare calls, so an ordinary session pays nothing
and a layout the user keeps returning to stays warm. It stops being
patient the moment the device refuses an allocation — at that point
holding an invisible viewport is what turns a degraded frame into a
session that cannot recover, and the operator needs the drawing to stay
usable long enough to close a viewport or two.

The cost of releasing a slot that is wanted again is one slow frame.

`slots-released n= urgent= slots=` reports when it happens, since the
alternative is inferring it from a memory number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:09 +03:00
PanosandClaude Opus 5 d81a858040 perf: account for the device memory the renderer holds
Stage 0 of the roadmap asks for a memory baseline before any more
optimisation, and explicitly not from RSS: the allocator and the driver
both retain high-water memory that RSS cannot tell apart from live
resources.

`gpu-live` counts from the sizes this renderer allocated, in the
categories large enough to decide where the next fix goes:

    slots  total  shadow  targets  text_atlas  wire_arena  block_geometry

It is partial by construction and says so, rather than presenting itself
as a total. Hatches, images, meshes and Face3D are not counted yet.

Three of the numbers needed a size that was not reachable before: the
block-geometry chunk records its bytes at build because in storage mode
the buffer belongs to the bind group, the arena exposes its allocation
rather than its occupied prefix — headroom is what a budget has to answer
for — and the text atlas reports its `R8` texel count.

Emitted only when a number moves. `prepare` runs thousands of times in a
session and these change a handful of times, so a per-frame line would
bury the log it is meant to explain.

This answers the question the previous fix left open: whether the arena's
52 MiB per slot, the 16 MiB shadow map every slot allocates whether or not
shadows are on, or the render targets dominate once a paper layout has
four or five viewports on screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:09 +03:00
PanosandClaude Opus 5 a9a2c82478 fix: share block definition geometry across viewport slots
Zooming a paper layout out until several viewports fit on screen ran the
card out of memory. The log correlates exactly: every error cluster
follows a `nav-prepare … space=Paper` with `viewports=3`, `4` or `5`,
where earlier sessions had reported `viewports=2` and stayed inside the
budget.

Each viewport gets its own `Pipeline` slot, and `block_geometry` was a
slot field — so every slot uploaded its own copy of the same definition
segments. That is the single largest upload this renderer makes: 1 101
definitions, 1 040 267 segments, 59.5 MiB. Three of them land back to
back in one burst in the log, each bracketed by its own full arena
rebuild.

The cache key is `(source_id, mesh_edge, colour, base translation)` —
nothing a viewport contributes. Two viewports drawing the same definition
draw the same segments, so the cache belongs beside `wire_buffer_cache`
on the `MultiPipeline`, which already shares the finished `BlockWireGpu`
batches across slots for the same reason.

That existing sharing also settles the one question this raises: bind
groups built against one slot's layout are already handed to other slots
today, so nothing new is being asked of wgpu here. Both GPU regression
tests still pass on a real adapter with validation scopes armed.

This does not address the wire arena, which stays per slot and rebuilds
52 MiB of instances each time a slot changes hands — `arena-base
ok=false` on every single build in the session, never `ok=true`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:09 +03:00
PanosandClaude Opus 5 d9c998fa17 fix: never cache a buffer the device rejected
After an out-of-memory the viewport stopped updating for the rest of the
session — both Model and Layout frozen, the UI still responsive. The log
shows why: `block-wire-geometry` prints *after* the errors it provoked.
The failed uploads were cached.

A rejected allocation still yields a `Buffer`. It is invalid, every later
use reports again, and it is indistinguishable from a good one at the
cache key. So the entry keeps hitting, nothing ever rebuilds, and the
frame stays empty even once memory frees up. Sharing `block_geometry`
across slots in the previous commit made that worse, not better: one
poisoned entry now serves every viewport.

The device's uncaptured-error counter was already there, private to the
handler that throttles the log. It is now readable, and the three caches
that can retain a bad upload — block definition geometry, the shared wire
buffer cache, and the arena's content id — snapshot it across their build
and decline to record what it condemns. The degraded frame still draws;
the next frame tries again.

Best effort by construction: wgpu surfaces some errors asynchronously, so
a moved counter means something failed, while an unmoved one is not proof
that nothing did. That asymmetry is the right way round here — believing
a bad buffer costs a frozen viewport, rebuilding a good one costs a frame.

`block-wire-geometry` gains `poisoned=` so a run says whether this
happened rather than leaving it to be inferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:53:09 +03:00
Hakan Seven af0cc68c5a Merge pull request #1006 with block rendering fixes
Invalidate cached world-space block vertices when the base translation changes. Share the regular/mesh partition between full uploads and patches so instanced blocks are uploaded only once. Cover both paths with CPU and GPU regression checks.
2026-09-05 12:30:43 +03:00
PanosandClaude Opus 5 fb64e76f04 fix: let the wire arena serve drawings that contain blocks
Clicking to end a segment froze the UI until the whole drawing had been
re-uploaded. With block geometry cached the remaining cost is the regular
half, and on two drawings it dominates:

    113 k wires   regular=96ms  block=13ms  index=12ms   total 122ms
    198 k wires   regular=85ms  block= 7ms  index=25ms   total 118ms

It is rebuilt from scratch because `PersistentWireArena` -- the only
thing that patches wire buffers in place -- was disabled outright by:

    && !vp_wires.iter().any(|wire| wire.render_instance.is_some())

One block reference was enough; the reproducer has 11 363. The scene
already produces an incremental patch in 0.9 ms and the GPU side could
not use it.

Instanced wires are not something the arena has to handle: they are a
different batch type with their own pipeline. Partition them out, let the
arena own and patch the rest, and keep building `BlockWireGpu` for them.

The handle index is unaffected -- it maps into `vp_wires`, not into arena
slabs, and the patch path already maintains it.

Measured on the 198 k-wire drawing, per drawn segment:

    before   wire 350ms  (shared-fullupload)
    after    wire  12ms  (arena-patch)

with the whole click, including `bump_entities`, going from ~411 ms to
~20 ms.

Verified visually on drawings with and without blocks, through repeated
edits and undo/redo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 09:18:50 +03:00
Hakan Seven 89624f64fc Merge PR #1002: opt-in performance instrumentation 2026-09-04 20:11:02 +03:00
PanosandClaude Opus 5 6747d7125e fix: no GPU allocation path maps, however small the buffer
The previous commit converted the allocations that scale with the
drawing and deliberately left the small fixed-size ones -- uniforms, the
viewcube's static geometry, per-image and per-wipeout quads -- reasoning
that an allocation failure was not a realistic outcome for a few dozen
bytes.

That reasoning was wrong, and the log says so precisely. On the
reproducer the vertex buffers degraded exactly as intended:

  [gpu] uncaptured ... Out of Memory
  [gpu] uncaptured ... In Queue::write_buffer
          Buffer with 'block_wire.vertices' label is invalid
  ... errors #4 #8 #16 #32 #64 #128, session kept alive

and then the process died here:

  thread 'main' panicked at wgpu_core.rs:2253:
  Error in Buffer::get_mapped_range: Validation Error
  Caused by: Buffer with 'block_wire.const' label is invalid

'block_wire.const' is a single uniform struct. Once the device is out of
memory every allocation fails, including a 32-byte one, and
create_buffer_init maps it and panics. Size was never the relevant
property -- whether the path maps is.

So every remaining allocation goes through gpu_upload too: block-wire
const, the viewcube's seven buffers, the blit and clip-boundary buffers,
image instances/params/vertices, mesh material params and surface and the
empty index stub, the texture-hatch uniform, and all five wipeout
buffers. `grep -r create_buffer_init src` now finds only a comment, and
no mapped_at_creation:true remains outside gpu_upload's own docs.

`queue` is threaded through the last holdouts: upload_wipeouts,
upload_clip_boundary, MeshBatchStubs::new and
create_material_bind_group_from_resources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:21:08 +03:00
PanosandClaude Opus 5 0d11421dea fix: free superseded wire buffers before allocating their replacement
The renderer shares one set of wire GPU buffers per wire_content_id
across every pane and viewport slot. Every geometry edit mints a new
content id, so every edit builds a new set -- and the old one stayed in
wire_buffer_cache until the map exceeded sixteen entries.

Sixteen entries is a sane bound for small drawings and fatal for large
ones. The policy counted entries, not bytes. On the reproducer a single
set is most of the card: the VRAM trace shows 572 MiB baseline rising to
1871 MiB on a 2048 MiB GT 1030. The second set has nowhere to go, so the
first edit after opening exhausts VRAM. The perf log shows exactly that
-- two `shared-fullupload` lines, an edit, then the process dies.

Evict on every miss instead of at a count threshold, and do it before
building the replacement rather than after, so the old allocation is
released while the new one is still being sized. Dropping this slot's own
handles first is what lets the retain see the superseded entry as
garbage; a pane still drawing that content keeps its own clone and its
strong count stays above one, so shared geometry is never pulled out
from under a live viewport.

The trade is that returning to a previous content id -- undo, mostly --
now rebuilds instead of hitting a warm entry. On a card where the
alternative is dying, that is the right way round.

Adds a PERF line reporting evicted and retained entry counts, so the
policy can be watched rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:21:08 +03:00
PanosandClaude Opus 5 d0ef752532 fix: an out-of-memory GPU no longer aborts the process
The crash in #358 was not the out-of-memory itself. wgpu reports OOM as
recoverable and the app already installs a handler that logs and keeps
the session alive. What killed the process was what happened next:

  create_buffer_init maps the new buffer unconditionally, and mapping the
  invalid buffer an OOM device returns panics. That panic unwinds through
  iced's main-thread redraw, trips the wgpu-hal swapchain assertion in a
  destructor, and becomes a non-unwinding abort -- no chance to autosave.

Every allocation whose size scales with the drawing now goes through
src/scene/pipeline/gpu_upload.rs, which creates the buffer unmapped and
fills it with Queue::write_buffer. A failed allocation then surfaces as a
validation error on the write, which reaches install_gpu_error_handler
and degrades the frame -- exactly what that handler's comment promised:
"A bad frame must degrade, not end the session."

Converted: block-wire vertices and instances, the wire const storage
buffer, all three wire-arena allocators, face3d vertices and instances,
block-text instances and text vertices, both hatch backends, and the
silhouette buffers. mesh_gpu already allocated this way, which is why the
mesh path never produced this panic; the rest now match it.

No mapped_at_creation:true remains in the renderer. The create_buffer_init
calls left are fixed-size or bounded-small -- uniforms, the viewcube's
static geometry, per-image and per-wipeout quads -- where an allocation
failure is not a realistic outcome.

Two trade-offs worth stating:

  * write_buffer stages through a copy, which mapping at creation avoided.
    Chunking bounds that transient cost, and the measured block-wire
    upload got faster with chunking, not slower.
  * `queue` had to be threaded through the wire, face3d, text and hatch
    builders and their callers. Mechanical, but it widens the diff.

This touches every upload path, so it needs a visual check against a real
drawing before it goes upstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:21:08 +03:00
PanosandClaude Opus 5 2787dcc1a3 perf: instrument the file-open load path behind PERF
The 52 s silent stretch in a large-DWG open could not be attributed:
OpenTimings covered parse / purge / xref / caches, but the FINALIZING
phase -- prepare_open_geometry, which does the first full Model
tessellation and builds the interaction index -- was unmeasured and
reports only two progress ticks, so it was indistinguishable from parse
time.

Add finalize_ms to OpenTimings and a per-stage PERF breakdown:

  open-phases     parse / purge / xref / caches / finalize
  open-finalize   tessellation vs interaction index
  open-index      index build vs prepare_screen
  wires-build     sort cache, visible filter, block cache, classify,
                  memo hits, parallel tessellate, materialize, sort
  resident-post   scene markers, refedit fade, resident layout
  wire-upload     build_wire_buffers, which runs on the render thread

The wires-build line separates the parallel tessellate from the serial
materialize loop that deep-clones every WireModel while the memo retains
a second copy -- the leading hypothesis for both the stall and the
6.45 GB RSS.

No behaviour change. Every timer is gated on crate::perf::enabled(), so
nothing runs without PERF=1 or the in-app PERF panel. The localized
open-summary format string is left alone; changing it would rekey the
string across every locale, so the full breakdown goes to PERF instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:20:30 +03:00
Karim Jerbi 688785a6cf feat(model-space): theme-aware model space canvas and CAD selection/grip sysvars
Model space appearance now follows the active UI theme instead of a
hardcoded dark background. Canvas mode is user-selectable (Match Theme /
Classic CAD Dark / Custom RGB), with matching controls for the paper
sheet and desk surround backgrounds and grid opacity.
Implements industry-standard CAD system variables, all settable via
command name, SETVAR, and the Options dialog:
- COLORTHEME (0/1 dark/light UI scheme, preserves custom palettes)
- COLORSCHEME (interactive named-theme selection)
- BACKGROUND (THEME/CLASSIC/DEFAULT/DESK presets, RGB triplets)
- SELECTIONAREA, SELECTIONAREAOPACITY (translucent marquee fill)
- WINDOWSAREACOLOR/WINDOWAREACOLOR, CROSSINGAREACOLOR (ACI or theme)
- SELECTIONEFFECT, SELECTIONEFFECTCOLOR (GPU highlight pass, driven by
  selection_generation invalidation — no retessellation)
- SELECTIONPREVIEW (bit 0 = idle rollover, bit 1 = in-command hover)
- GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER (grip overlay styling)
2026-09-03 17:44:44 +01:00
Hakan Seven 2cd1d36661 fix(render): match model lineweight preview curve 2026-09-02 23:28:34 +03:00
Hakan Seven 76bdab1e5b feat(display): add model lineweight scale 2026-09-02 23:03:04 +03:00
Hakan Seven edbc4d8ff8 fix(render): correct lineweight display scaling
Remove the global display boost and scale paper-space lineweights with the current sheet zoom across wire, block-wire, and hatch paths.

Fixes #962
2026-09-02 22:32:53 +03:00
ramox81 a706858ff1 Use solid edges for normal selection 2026-09-02 14:38:22 +03:00
Hakan Seven 7ea4f338f6 fix(render): preserve transparency inheritance
Resolve ByBlock alpha independently from color and keep explicit opaque values from falling back to layer transparency.

Pin the matching cadcodec API across desktop, plugin, and web targets.

Refs HakanSeven12/cadcodec#2
2026-08-31 22:48:57 +03:00
Hakan Seven 853e830126 fix(grips): prevent phantom snapping
Keep hidden pre-drag geometry out of snap candidates and avoid empty
scene invalidations. Require stable OTRACK dwell observations and cache
planar-solid visibility during live previews.

Refs #930
2026-08-30 22:26:47 +03:00