Add tests/spec_wire_consistency.rs: (a) every op/query name in spec/entities.toml
maps to an existing Operation/Query variant; (b) both enums are append-only
relative to a recorded variant-order baseline (bincode discriminant stability).
Adds toml to dev-dependencies. 3 tests green.
Add Arc/Ellipse/Spline/Ray/XLine arms to the test + example mock add_curve
kind matching and the example curve_bounds, so the crate's unit tests and
examples compile against the Phase 2 curve families.
Reuse the application library from the binary entry point instead of compiling the module tree twice. Keep desktop-only transports and caches out of web builds, remove unused test code, and check executable targets in CI.
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.
`PersistentWireArena` keeps one instance buffer for the whole resident
set and patches it in place on an edit. Sizing that against the shared
per-chunk budget caps it at 32 MiB, which is 524 288 `WireInstance`s.
A drawing with more than that fails `WireArena::build`, falls back to the
batched path, and re-uploads everything on every edit. On the 198 k-wire
reproducer -- 821 316 instances -- that shows up as:
wire 200-370ms arena-hybrid
instead of the ~12 ms `arena-patch` the same drawing reached before the
limits moved onto the shared budget.
The 32 MiB cap is right for transient chunks, where many are allocated
per frame and any one of them failing loses the frame. The arena's buffer
is a single long-lived allocation that exists precisely to avoid those
re-uploads, and it can afford more: 821 k instances is 53 MB, ~79 MB with
the arena's headroom. 128 MiB leaves it four times smaller than the
505 MiB allocation that crashed a 2 GB card, and it is allocated through
`gpu_upload`, so exhausting VRAM still degrades the frame rather than
aborting.
`OCS_GPU_CHUNK_MIB` overrides both caps, so a test can still force small
buffers everywhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Adding one entity to a drawing rebuilt every block group's geometry from
scratch. Measured on a 1.17 M-entity drawing with 11 380 block wires, the
per-edit upload split as:
upload-split regular=80.4ms block=230.3ms
The block half is three quarters of it, and every byte of it was
identical to the previous frame: a new line is not a block reference.
`BlockWireGpu::from_wires` re-emitted each group's segments, re-expanded
them six-fold and re-uploaded the vertex buffers anyway.
That work depends only on the block definition being expanded, which
already has a stable identity: `render_instance.source_id`, minted once
when the block cache expands a definition and unchanged while that
expansion stays cached. Cache the vertex chunks and constants under
`(source_id, mesh_edge, colour)` -- colour is in the key because a
highlight tint changes the constants while leaving the vertices alone.
Only the per-instance placements are rebuilt, and they must be: each
carries its own draw depth and the depth map shifts on every add or
remove. They are also small, a few thousand placements against millions
of vertices.
Entries not seen in a build are dropped, so a block-cache rebuild that
mints fresh source ids does not strand old buffers on the GPU.
Measured: the block half falls from 230 ms to 7.4 ms, and the whole
per-edit upload from ~350 ms to ~118 ms.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the block geometry cached, the per-edit upload is 135 ms of which
only 92 ms was accounted for by the regular and block halves. The rest is
the handle index: a `String::parse` per wire over the whole 198 k
resident set, rebuilt from scratch on every edit.
The arena has an incremental path for this (`patch_handle_index`), but
the shared path never reaches it -- the same gate that keeps the arena
away from this drawing. Short of that, the parses are independent, so run
them across cores and keep only the map insertion serial.
`par_iter().enumerate()` collects in index order, so the slot lists keep
the emission order they depend on.
Adds a `wire-handle-index` PERF line, since this was the unaccounted
third of the upload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spans put it exactly: 63.5 ms of a 63.8 ms pointer-up is
`bump_entities`, and inside it two drawing-wide scans.
`refresh_associative_centerlines` and `refresh_associative_center_marks`
each walk all 1 166 357 entities, filter to Lines and parse every one's
extended data looking for an association that references a changed
handle. About 30 ms each, run on every edit -- to find, on this drawing,
none at all.
Track whether the document holds any such association. The question is
settled once, with a single pass, and after that each edit only examines
the handles it actually changed: an entity that carries an association
flips the answer to yes, and nothing else can. Deleting the last one
leaves the answer at yes, which costs a scan that finds nothing rather
than skipping one it should have run. A coarse `bump_geometry` re-opens
the question, since it can carry a new document.
Where an association really exists the scans run exactly as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warming only the interaction handle index left the first hover at
385 ms, down from 992 but not gone:
hover-detail query=0.1 handles=384.6 ... candidates=186
The handles stage builds two indexes, not one. `interaction_candidate_handles`
also reaches for `entity_index()` — the entity quadtree, keyed on
geometry_epoch and just as cold at that point. Warm both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first edit on a large drawing spent 1147 ms rebuilding the block
definition cache:
wires-build 2723.3ms ... blk_cache=1147.2 ... memo_miss=186455
Not because the edit invalidated it -- `add_entity_internal` already
routes a non-block entity through `bump_entities`, so drawing a line
leaves `block_epoch` alone. The cache was empty because
`prepare_open_geometry` built it on a throwaway `Scene`, exactly as it
did the tessellation memo, and it went the same way.
`PreparedOpenGeometry` now carries the definitions across, moved rather
than cloned, and `install_prepared_open_geometry` stamps them with the
receiving scene's `block_epoch`. That stamp is sound: the document is not
touched between the loader finishing and this call, so the definitions
describe it exactly. A key that does not match what the scene later asks
for simply misses, which is the old behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first hover over the canvas after opening a large drawing froze the
UI for a second:
hover-detail query=0.2 handles=986.1 ... candidates=253
hover-dwell 986.6ms
`interaction_handle_index` and the insert-hatch map it reads are built
lazily, and first use is that hover. Neither can move to the loader
thread: they read `hatches`, `meshes` and `block_meshes`, which are
installed on the real `Scene` only after the loader has finished.
Build them at the end of the open instead, where the user is already
waiting. This does not make the work cheaper -- open grows by roughly the
second the hover used to cost -- but a second inside an operation that is
already seconds long reads very differently from a second-long freeze on
a mouse move.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`prepare_open_geometry` builds the first Model wire set on a throwaway
`Scene`, filling its per-entity tessellation memo with an entry for every
one of the drawing's 186 455 entities. `install_prepared_open_geometry`
then transferred the wire set and the interaction index but not the memo,
so the temporary scene took it to the grave.
The first geometry edit therefore rebuilt from cold:
wires-build 2723.3ms memo_hit=0 memo_miss=186455
against 760 ms once the memo is warm -- 1.3 s of work the loader thread
had already done, repeated on the user's first action.
`PreparedOpenGeometry` now carries the memo, moved rather than cloned
since the scene is dropped immediately afterwards. The guard hash travels
with it: the memo is only usable under the state it was built for, and a
mismatch on the receiving side clears it, which is what happened
unconditionally before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The resident wire sort keys on the entity handle parsed out of each
wire's name `String`. `sort_by_key` evaluates that key on every
comparison, so a 198 k-wire resident set parsed every name roughly
eighteen times over.
Measured at 295 ms on every rebuild of the resident set -- including
rebuilds that re-tessellated nothing, where the memo hit on all 186 455
entities and the sort was most of what was left.
`sort_by_cached_key` evaluates the key once per element. It is stable in
the same way `sort_by_key` is, so draw order is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Add 'i ' prefix to EntryKind::Info and coordinate set_step_prompt deduplication to satisfy WCAG 1.4.1 across all 22 themes
- Drop unused _desk parameter from crosshair_background and clean up its call sites
- Document AutoCAD-standard context-based crosshair drafting surface architecture in crosshair_background
- Add doc comments documenting deliberate alpha omission in canvas_is_light and backlog scope in is_wide