BlockWireGpu::from_wires built one unbounded vertex buffer per
(source_id, mesh_edge) group. On a block-heavy drawing that reached
~505 MiB in a single create_buffer_init, which fails on a 2 GB card;
wgpu then returns an invalid buffer and the unchecked
get_mapped_range_mut() inside create_buffer_init panics.
The instance buffer in the same function was already chunked. Convert
the vertex buffer the same way, matching the solid-mesh chunking in
mesh_gpu.rs (#203) and the fill chunking in face3d_gpu.rs (#358),
including mesh_gpu.rs's clamp: max_buffer_size is an API validation
limit, not physical VRAM, and NVIDIA reports several GB on a 2 GB card.
Chunk sizes are rounded down to a multiple of 6 so a segment's quad
never straddles two buffers -- the shader derives its corner from
vertex_index % 6 and every draw restarts that counter at zero.
Instance buffers are hoisted out so they are built once and reused
across vertex chunks instead of re-uploaded per chunk.
Co-Authored-By: Claude Opus 5 <[email protected]>
Everything measured so far accounts for about 1 ms of a ~120 ms pan
frame: update is 5-28 ms at worst and usually far less, prepare 0.45 ms,
encode 0.5 ms. Meanwhile the GPU idles at 0-8%, encoding no scene at all
leaves latency unchanged, major page faults stay flat at 17 across forty
seconds of panning, and freeing 6 GB of swap changes nothing either.
So the time is in code that has never been timed. `view()` runs between
the input handler returning and `prepare` being called, rebuilds the
whole widget tree every frame, and is the largest unmeasured span in
that gap.
Reports only when it exceeds 1 ms, so a healthy frame stays quiet.
Co-Authored-By: Claude Opus 5 <[email protected]>
Pan and even plain cursor movement over the canvas ran at ~8 fps on a
large drawing. Nothing rendered was responsible: the GPU idled at 0-8%,
encoding no scene at all changed nothing, and vertices, pixels and draw
calls could each be cut without moving the number. `view` was the frame
-- 110 ms of it against 3-6 ms on a small drawing -- and inside `view`
it was the status bar.
The status bar builds the selection-filter menu, which asks for the
distinct entity type names in the layout. That call cloned the layout's
entire handle list (1 166 357 handles, ~9 MB), then did a map lookup, a
String allocation and a BTreeSet insert for each one. About 107 ms of a
110 ms frame, to produce roughly twenty names, for a menu that is only
visible when opened.
Cache the result against geometry_epoch and the layout block, since it
only changes when entities are added or removed, and make the scan
itself borrow the handle list rather than clone it and collect &str,
allocating only for the names it returns.
The immediate-mode UI rebuilds every widget on every mouse move, which is
fine as long as nothing in that path is O(document). This was.
Co-Authored-By: Claude Opus 5 <[email protected]>
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 <[email protected]>
Refresh stale depth bounds after geometry edits, allow orbiting through camera poles, and preserve hidden-line occlusion for selected solids.\n\nFixes #957
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)
parse_power accepted a single exponent (`unary ('^' unary)?`), so a
chained power stopped after the first `^`. The remaining `^2` was left
unconsumed, and eval_to_string's embedded-expression scan then handed it
back as literal text: typing `2^3^2` into a coordinate or distance field
produced the string "8^2" rather than 512, and `2^3^2+1" produced "8^3".
Recurse into parse_power for the exponent so `2^3^2` reads as
`2^(3^2)", the standard right-associative meaning, and the whole
expression is consumed.
DIMAUNIT 4 is Surveyor's units, but format_angular_value matched only
0-3 and let 4 fall through the catch-all to decimal degrees. An angular
dimension in a drawing authored with surveyor units therefore read as
plain degrees, while AUNITS 4 in the same drawing writes a bearing for
the coordinate readout — the two disagreed on one angle.
Format the bearing from the dimension's own DMS path so it carries
DIMADEC, DIMAZIN and DIMDSEP, quantise before picking the quadrant so a
value just short of a cardinal does not quote a quarter turn, and offer
the option in the dimension-style dialog.
The Properties panel reads and writes ACAD_DSTYLE overrides for all three
dimension colours (DIMCLRD 176, DIMCLRE 177, DIMCLRT 178), but the
renderer consulted the override only for DIMCLRD and took DIMCLRE/DIMCLRT
straight from the dimension style. Editing 'Text color' on a dimension
therefore showed the new colour in the panel while the drawing kept
rendering the inherited style colour — which for the common
DIMCLRT = ByLayer default is the layer colour. (#898)
Route all three through one helper so the renderer and the panel read the
same value.