Keep storage fast paths on capable adapters and use packed or texture fallbacks only when device limits require them.\n\nCompact mesh-edge varyings to avoid WebGL shader rejection.
Avoid full document, cache, and GPU rebuilds for entity deltas so add, delete, history, selection, and live polyline edits stay responsive on large drawings.
Keep resident wires, draw depths, GPU slabs, and edit history incremental across entity changes. Defer rollover work to avoid cursor jumps on dense drawings.
Xref merge now inserts entities in the source block's chain order and
carries SortEntitiesTable objects across (remapped to merged handles).
draw_depth_map values become [depth, half] so exploded block/xref fills
and wide-polyline band wires both compose child depth as
insert_depth + in-block rank x half, recursing into nested inserts with
the same divisor - bands and fills from one block interleave by the
block's internal order instead of stacking at the insert's single depth.
Band batches split per rank via WireModel.depth_override; thin wires
stay merged. The wire arena falls back to a full rebuild when a slab
carries mixed depths so refreshes don't flatten band overrides.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A wide polyline with per-vertex varying widths rendered at its widest edge
(uniform-width ones were already exact). Draw the true taper instead.
`WireModel.taper_widths` carries a per-point full band width. The wire
instance (native slim + web fat) gains per-endpoint `world_hw_a/b`, and both
wire shaders interpolate `mix(hw_a, hw_b, which_end)` so each segment's band
narrows/widens linearly — depth-tested, and the linetype dash still applies.
Alignment (widths must match the tessellated points) is solved by building
both together: a new `TruckObject::TaperedLines(points, widths)`, filled by
`common::tapered_band_points` (straight = endpoints, arc = 16 samples with the
width lerped along it). to_truck routes a varying-width polyline here —
LwPolyline before the PLINEGEN split, Polyline2D at its return — while a
uniform-width one keeps the constant-band Contour path unchanged. `world_width`
stays the widest edge as the PDF-stroke + zoom-floor fallback.
Blocks fall back to the constant-widest band (`LocalWire` carries no
taper_widths).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rectangular paper viewports now go through the same GPU stencil clip as
non-rectangular ones: viewport_clip_boundary_ndc returns the four rect
corners (a full-target mask) instead of nothing, so the render path has
no rect/non-rect special case — every content viewport carries a clip
boundary and draws with stencil reference 0xFF.
With a single clip path, the old per-wire / -image / -wipeout scissor
mechanism is fully dead and removed: compute_*_scissors, the
*_pixel_scissors buffers, the project_scissor helper, the per-pass
set_scissor_rect blocks, the vp_scissor field threaded through
WireModel / ImageModel / HatchModel and their GPU mirrors (WireGpu /
ImageGpu / WipeoutGpu / HatchWebGpu), from_run's scissor parameter, the
scissor grouping in build_wire_buffers, and the is_arena_eligible gate.
Net -245 lines across 54 files.
Rectangular viewports still clip exactly via their own render rectangle;
the stencil mask is a redundant full-target no-op there, kept only so
the clip code path stays uniform. XCLIP keeps its CPU wire clip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-batch arena required a set with no mesh/solid edges, so any drawing
with a 3D solid or mesh (this one: 1693 mesh-edge wires of 12200) disqualified the
WHOLE set — every edit fell back to build_wire_buffers re-uploading ~265 MB
(4.4M instances), stalling ~1s per edit on a shared-memory GPU.
Split the resident set into a regular-2D arena and a mesh-edge arena (the wires
that need the draw loops mesh-edge skip/black treatment), one WireArena each,
passed as &[&WireModel] subsets (no clone). Both patch incrementally; the mesh
arena carries is_3d_mesh_edge so the existing draw loop handles it. A changed
handle absent from an arenas subset is a no-op for it, so each edit touches only
the batch it belongs to. Result: the wire block drops from ~300ms + a 265 MB GPU
upload to ~2ms.
Two correctness points folded in from adversarial review:
- mesh-edge wires are stamped draw_depth 0 (matching WireGpu::from_run) so their
hidden back edges dont peek through the shaded fill; the mesh arena also always
bails on append (all its wires share depth 0, so relocation would reorder
coincident edges).
- buffers are capped at wgpus 256 MB limit — a batch larger than one buffer
returns None so the caller chunks it via the batched path (fixes a create_buffer
panic on the 355 MB regular batch).
Still behind OCS_WIRE_GPU_PATCH (default off). OCS_PERF prints the per-edit wire
timing + outcome.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The in-place-only arena still fell back to a full batched rebuild on add/remove —
re-uploading the entire wire instance buffer, which on a drawing whose wires
expand to millions of segments (~265 MB) stalls ~1s per add/remove on a
shared-memory GPU. Only move/edit avoided it.
Add append/tombstone so add/remove patch the instance buffer too: a new or
segment-count-changed entity bump-allocates a slab at the tail (buffers carry
1.5x headroom); an erased entity is tombstoned (blanked at const slot 0). The
huge instance buffer only grows/blanks by the changed entity.
Three correctness points make add/remove safe (each verified by an adversarial
review, findings folded in):
- draw_depth_map normalises each entity`s z-bias by the block entity count, so a
structural change re-scales every entity`s bias. Keep a CPU mirror of the const
array; on any structural change refresh every slab`s draw_depth and re-upload
the small (~1 MB) const buffer, leaving the instance buffer untouched.
- a tail-appended entity draws last, which only mis-orders wires whose overlap
resolves by submission order rather than the z-bias: transparency, and 3D-solid
fallback edges (Solid3D/Region/Body/Surface are excluded from draw_depth_map, so
their opaque edges share depth 0). `append_unsafe` bails to a full rebuild when
the set contains either.
- an in-place edit also flushes its consts to the GPU (a colour change is not
structural, so the whole-buffer refresh would otherwise skip it).
Still behind OCS_WIRE_GPU_PATCH (default off); every uncertain case returns false
and the caller does the identical full rebuild, so correctness never rides on the
fast path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a geometry edit the resident set gets a fresh content id, so the batched wire
path (build_wire_buffers) re-emits and re-uploads EVERY wire instance — the whole
buffer even when one line moved. Behind the OCS_WIRE_GPU_PATCH flag (default off,
native only), add a persistent per-entity instance arena: one long-lived
VERTEX|COPY_DST instance buffer + its WireConst storage, with a per-entity slab
table. An in-place edit (move / rotate / scale / colour — same segment count)
becomes a queue.write_buffer of just that entity`s slab.
Scope is deliberately IN-PLACE ONLY. draw_depth_map normalises each entity`s
draw-order z-bias by the block`s entity count, so an Add or Remove re-scales EVERY
entity`s z-bias — which a per-slab patch can`t honour — and a segment-count change
relocates the entity. patch() returns false for any of those (and for mesh-edge /
scissored sets), and the caller falls back to the identical full batched rebuild,
so correctness never rides on the fast path. An adversarial review of an earlier
append/tombstone design surfaced exactly these draw-order hazards; restricting to
in-place removes them while keeping the common interactive-edit win.
The Scene hands the changed handles to the arena via model_wire_gpu_patch,
recorded by try_resident_patch for the Model block and matched by content id in
prepare(); scissored paper viewports and mixed 2D/3D sets keep the batched path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>