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>
Keep fractional raster placement and matching integer texture coverage
through compositing. Apply parent clipping without changing the scale.
Fixes#1267
A day of pan/zoom that had suddenly become unusable on a 1.2M-entity
drawing turned out to be no regression at all: an unattended NVIDIA
userspace update had been applied without a reboot, the Vulkan ICD could
no longer talk to the still-loaded kernel module, and wgpu silently fell
through to Mesa's llvmpipe. Nothing in the app said so. iced picks the
adapter and names it only under RUST_LOG, so the only evidence was 32
`llvmpipe-N` threads in /proc. The same silence covers WARP on Windows,
SwiftShader in browsers, and the case where wgpu finds no adapter at all
and iced quietly draws the chrome with tiny-skia while the shader widget
is dropped, leaving a blank viewport. This commit makes all of that
visible, on every platform, without new dependencies.
Detection (src/scene/pipeline/mod.rs)
-------------------------------------
`GpuStatus` is derived from two signals:
* `record_gpu_adapter`, run from `MultiPipeline::new` (once per device,
next to the existing uncaptured-error handler), reads
`Device::adapter_info()`, prints one `[gpu] adapter: <name> (<backend>,
<device type>, driver …)` line — stderr natively, `console.warn` on the
web, which has no stderr — and keeps a copy.
* `note_scene_draw`, run from `ViewportPane::draw`, counts how often the
viewport asked for a primitive. Every renderer makes that call; only
wgpu goes on to `prepare` and create a device. Three draws with no
device ever recorded therefore mean the primitive is being dropped.
The verdict is one of `Unknown`, `Hardware`, `Software(adapter)` or
`NoRenderer`. `Software` is `DeviceType::Cpu` — what Vulkan reports for
llvmpipe, DX12 for WARP and GL for a renderer string that gives it away —
plus a name match on the fallbacks browsers expose (SwiftShader, llvmpipe,
lavapipe, "Microsoft Basic Render"), because the WebGPU backend reports
every adapter as `Other` (gfx-rs/wgpu#8819). Metal has no software device,
so on macOS only `NoRenderer` can occur (a VM without a GPU); it is caught
by the draw count like everywhere else.
`gpu_status_if_changed` costs one atomic load per app message: a
generation counter moves only when a device is recorded or the draw count
crosses its threshold, so the app re-derives the verdict a handful of
times per session.
Notification (src/app/startup.rs, view/modal.rs, ui/statusbar)
-------------------------------------------------------------
`refresh_gpu_status` runs after every message. On a degraded verdict it
speaks through three channels, because many users never read the command
line:
* A popup, `ModalKind::GpuWarning`, queued once per verdict through the
startup-modal queue so it waits its turn behind the file open. It names
the adapter, states the consequence (slow navigation, unstable or
missing rendering) and gives the remedy that usually applies on the
running platform: a driver update not yet followed by a reboot on
Linux, a missing driver or a Remote Desktop / VM session on Windows, a
VM without a Metal device on macOS, hardware acceleration switched off
in the browser. "Don't show again for this device" stores
`GpuStatus::identity()` in `settings.gpu_warning_silenced`; keying it by
verdict means silencing "software rendering on llvmpipe" does not
silence a later, different failure.
* A status-bar pill in the theme's warning colours, `⚠ Software rendering`
or `⚠ No GPU renderer`. It is not a `StatusPill`, so it cannot be hidden,
and it stays for the session; clicking it reopens the popup, silenced or
not.
* A command-line entry through the new `CommandLine::push_warning`, which
is styled like an error but deliberately does not touch `last_error` /
`error_revision`: automation reads those to decide whether the command it
just ran failed, and a graphics warning is not a failed command.
`subscription()` requests frames only while the verdict is `Unknown` on a
document tab (the Start page draws no viewport), so the probe costs one
frame on a working GPU, three on none, then stops.
Verification
------------
* Live on the machine that had the incident, with the Vulkan loader
restricted to Mesa (`VK_DRIVER_FILES=…/lvp_icd.json WGPU_BACKEND=vulkan`):
popup → OK → pill → click reopens; "Don't show again" → restart → pill
only, no popup.
* `gpu_tests::the_adapter_is_reported_once_per_device` (ignored, needs an
adapter) asserts the classification on a real device; it was run on both
llvmpipe (`Cpu`) and an NVIDIA GT 1030 (`DiscreteGpu`).
* Unit tests cover the name heuristic, the verdict function, the identity
rule, the warning text with an adapter name that itself contains
parentheses and commas, the settings round trip and the command-line
semantics. New strings are in all 21 locales as the `gpu` Fluent group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Repurpose CircleInstance center_high.w and params.w for start_width and end_width (preserving exact 128-byte stride)
- Route wide polylines, tapered polylines with bulges, and donuts through split_mixed_polyline into GPU analytical arc instances
- Compute physical annular bands, width tapers, and round end caps in circle.wgsl using world-space distances to prevent 3D perspective foreshortening distortion
- Expand bounding quad margins dynamically with grazing-angle compensation to eliminate clipping
- Relax pick_tris check in wire classification and partition routines so wide entities retain CPU picking while drawing analytically
- Add unit and hardware GPU integration tests, including tilted 3D perspective rendering verification
- Fall back to WireModel::SELECTED when selected_tint is None (classic mode) so selected circles, arcs, and ellipses are not omitted from the highlight pass.
- Explicitly rebind uniform_bind_group before drawing analytical curves in selection_xray pass.
- Skip invisible wires (display_visible == false) during selection and hover wire extraction.
`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>
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>
The layout-switch OOM names its buffer: `Buffer with 'block_wire.vertices'
label is invalid`, right after `uncaptured wgpu error #1 … Out of Memory`.
That buffer is the largest thing this renderer allocates, and five sixths
of it is duplication.
A segment reaches the shader once per corner of its quad, and the packed
path satisfies that by writing the whole 60-byte record six times —
360 bytes a segment — with `vs_main` recovering the corner from
`vertex_index % 6`. Six identical copies, on a 2 GB card.
`block_wire_storage.wgsl` puts the segments in a read-only storage buffer
indexed by `vertex_index / 6` instead, so each record is stored once. The
geometry vertex buffer disappears; the placements move to slot 0 and the
segments arrive through the definition's bind group, which becomes
per-chunk because each chunk binds its own slice.
This is the pattern `wire_indexed.wgsl` already uses for per-wire
constants, down to the capability gate: `supports_wire_storage` asks for
exactly one vertex-stage storage binding, which is exactly what this
needs. Where it is false — WebGL, the forced compatibility renderer — the
packed shader and its vertex buffer are built as before.
`Segment` is declared with scalar members rather than `vec3<f32>`, which
would align to 16 in storage and reintroduce the padding this removes. A
test validates the shader through naga and asserts the struct's stride
equals `size_of::<BlockWireVertex>()`, because a layout mismatch would
otherwise surface only as garbage geometry on a real device.
`block-wire-geometry` reports the bytes uploaded and what the packed path
would have taken, so the saving is measured rather than assumed.
The cache value becomes `Arc<Vec<BlockGeometryChunk>>` while keeping the
translation-aware key from #1006 — both modes still have to invalidate
when the base instance moves, because the records hold its world
coordinates either way. `const_bgl` and the mode are decided together
when the pipeline is built, so they travel as one `BlockWireTarget`
rather than pushing `from_wires` past the argument limit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.