Commit Graph
221 Commits
Author SHA1 Message Date
Hakan Seven 3253965194 Run wire arena regressions in CI and shorten cache documentation 2026-09-05 12:33:20 +03:00
Hakan Seven ff3262e39d Merge commit '7bf095e02be49764fdda1fa32ef13a1675592246' 2026-09-05 12:30:43 +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 19490a548b fix: give the wire arena its own buffer budget
`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 <[email protected]>
2026-09-05 09:50:17 +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 <[email protected]>
2026-09-05 09:18:50 +03:00
PanosandClaude Opus 5 071b4b6a87 perf: keep block geometry on the GPU across edits
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 <[email protected]>
2026-09-05 09:16:12 +03:00
PanosandClaude Opus 5 7bf095e02b perf: parse wire names across cores when building the handle index
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 <[email protected]>
2026-09-05 09:10:49 +03:00
Hakan Seven 9d991d7feb Bound GPU uploads and complete Greek and performance integration 2026-09-04 20:26:29 +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 <[email protected]>
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 <[email protected]>
2026-09-04 16:21:08 +03:00
PanosandClaude Opus 5 771a5b2c5c fix: one VRAM-aware buffer budget for every GPU upload path
max_buffer_size is an API validation limit, not physical VRAM. NVIDIA
reports several GB of it on a 2 GB card, so sizing a chunk from it
produces buffers larger than the whole device. That is the same bug as
#203 (mesh_gpu), #358 (face3d_gpu) and the block-wire vertex buffer:
rediscovered three times because each upload path spelled the budget out
for itself.

mesh_gpu.rs was the only site that clamped the result. Move that clamp
into src/scene/pipeline/gpu_budget.rs and route every allocating path
through it, so a new upload path cannot reintroduce the mistake by
copying one of the unclamped expressions.

Five sites had no clamp at all: text_gpu (block-text instances),
wire_gpu (block-wire instances), and face3d_gpu (vertices, instances,
and upload_chunks).

Three behaviour changes worth noting:

  * Silhouette chunks were capped at a private 64 MiB constant and now
    use the shared 32 MiB budget.
  * The storage-hatch chunk limit was the minimum of two device limits,
    both of them validation figures; it is now also capped by the
    budget. The `*_fits` predicates next to it are genuine "would wgpu
    reject this" checks and are left alone.
  * mesh_gpu's instanced path used the unclamped ceiling to decide
    whether a mesh fits in a single buffer, which would still admit a
    multi-GB allocation. It now uses the clamped budget, so a few more
    meshes take the chunked path.

OCS_GPU_CHUNK_MIB overrides the cap. Raising it above the real VRAM is
the cheapest way to force an allocation failure and check that the
renderer degrades instead of aborting.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 16:20:38 +03:00
PanosandClaude Opus 5 8552450aa8 fix: chunk block-wire vertex buffers
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]>
2026-09-04 16:20:38 +03:00
Karim Jerbi 886d5777d8 fix(ViewCube): WCS dropdown no longer appears to clip view cube
- enlarged render area for the viewcube container, and increased the gap between cube region and WCS dropdown
2026-09-04 10:21:33 +01:00
Hakan Seven 3088e641aa fix(view): stabilize 3D navigation and hidden lines
Refresh stale depth bounds after geometry edits, allow orbiting through camera poles, and preserve hidden-line occlusion for selected solids.\n\nFixes #957
2026-09-03 20:22:35 +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
ramox81 059e219cb6 Fix sphere edge rendering 2026-09-01 09:16:29 +03:00
ramox81 2768efeed5 Fix mesh wireframe vertex layout 2026-08-31 18:01: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
Hakan Seven 81e7add1dd fix(tolerance): complete workflow integration
Sync current main and companion codec changes before preserving unsupported frame rows, rendering in the entity plane, and sharing final layout metrics with placement preview.
2026-08-27 00:14:08 +03:00
Hakan Seven fd801fac1b fix(hatch): render weighted pattern strokes
Show styled boundaries only while selected.\n\nRefs #821
2026-08-25 22:44:51 +03:00
Hakan Seven 4209c35ba3 fix(polyline): render fill mode boundaries
Use kernel-produced band outlines when FILLMODE is off. Preserve centerline linetype stations through inserts, clipping, projection, and PDF export.
2026-08-24 22:18:44 +03:00
ramox81 8c56c03387 fix(polyline): honor global width and fill mode 2026-08-24 09:36:57 +03:00
Hakan Seven 7293bec1b9 fix(point): preserve relative marker sizing
Keep rendering, picking, block transforms, and plotted output on the
same viewport-relative PDSIZE geometry without overloading wire width
fields.
2026-08-22 22:22:01 +03:00
ramox81 c5fe70627a Honor point display geometry and sizing 2026-08-22 17:18:30 +03:00
Hakan Seven ee89458f8a fix(solid): use kernel geometry
Derive extrusion, bounds, edges, and fill from cadkernel while preserving precise view filtering and stable GPU uploads.
2026-08-21 19:47:03 +03:00
ramox81 17dc0aff39 Complete 2D solid workflow 2026-08-21 10:14:26 +03:00
Hakan Seven 87193ce22e fix(image): preserve raster brightness 2026-08-21 00:33:28 +03:00
Hakan Seven 44efb8fd53 feat(frame): complete frame controls
Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities.

Refs #642
2026-08-20 23:37:02 +03:00
Hakan Seven 2a852f4479 feat(gradient): align editing and rendering 2026-08-20 21:17:10 +03:00
ramox81 aee3397f5d fix(gradient): align properties and editing 2026-08-20 15:33:09 +03:00
Hakan Seven 8601fd8e4b fix(render): draw block-contained 3D faces 2026-08-19 23:34:10 +03:00
Hakan Seven c8840e2706 fix(web): force WebGL2 backend
Affected Chromium adapters fail after selecting WebGPU even when packed renderer paths are forced. Use WebGL2 for the web app and let device limits select renderer tiers.\n\nRefs #788
2026-08-19 19:04:58 +03:00
Hakan Seven 97a6322e2a fix(web): use compatibility renderer
Chromium can advertise WebGPU limits that its shader path does not reliably support. Keep web builds on the packed/texture pipelines.\n\nRefs #788
2026-08-18 22:35:39 +03:00
Hakan Seven 36e0e40b36 perf(raster): avoid needless image resampling 2026-08-16 21:22:58 +03:00
Hakan Seven e58bef39dc fix(web): use browser-safe render timing 2026-08-13 23:57:51 +03:00
Hakan Seven 5b9fffdfba fix(hatch): clip fill with kernel mesh
Resolve boundary topology before rendering so corner-aligned pattern rows cannot flip shader parity. Chunk indexed geometry to device buffer limits and retain the texture compatibility transport.

Refs #489
2026-08-13 00:29:03 +03:00
Hakan Seven 5e8221f8f1 perf(render): keep solid meshes resident
Keep all solid geometry GPU-resident during navigation. Use vertex-rate instancing, compact untextured vertices, vertex-cache ordering, and cached silhouette sources without culling geometry.
2026-08-12 16:02:10 +03:00
Hakan Seven 7b4af0aa75 fix(render): share repeated block meshes
Keep placement transforms and bounds separate from immutable mesh data to prevent large drawings from exhausting memory or stalling selection.
2026-08-12 11:01:53 +03:00
Hakan Seven 4fd9b98da4 perf(render): instance block geometry
Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
2026-08-12 02:33:10 +03:00
Hakan Seven a1f64d7663 fix(render): restore solid surface rendering
Keep viewport styles from disabling selected shaded modes.\n\nPack mesh shader inputs to stay within WebGL limits.
2026-08-11 15:44:40 +03:00
Hakan Seven ac8183ec91 refactor(cad): use direct stack dependencies 2026-08-10 15:03:31 +03:00
Hakan Seven dc998fe46b refactor(tess): move solid sampling to kernel
Remove local face, edge, spline, sweep, loft, and silhouette
samplers. Meshes and overlays share the kernel tolerance and edge
schedule.
2026-08-10 10:18:53 +03:00
Hakan SevenandClaude Opus 5 62dfbfe2be fix(paper): give the sheet back its desk and its edge
A paper layout painted its whole canvas in the page colour, so the sheet
had no visible edge — zooming out never reached one, because the surround
was the same colour as the page. Floating viewports then painted
themselves black over it.

Both come from the same idea, that a viewport's canvas is a colour it can
be handed. What lies behind a viewport depends on what the viewport is:

- The full-canvas sheet shows the desk. `paper_bg_color` is the page, and
  the page is a bounded rectangle `paper_sheet_fill` already draws; using
  it for the canvas too erased the boundary between them. The desk is now
  its own colour, defined once in the scene and read by both the widget
  and the renderer, so the two cannot drift apart.
- A floating viewport sits on that sheet and must paint no canvas at all.
  Handing it a transparent one does not work: the background shader
  returns an opaque colour whatever alpha it is given, so alpha zero came
  out black. Skipping the pass is the only way to ask for see-through, so
  `skip_background` now suppresses both the clear and the pass. It is part
  of the frame signature too — without that, a cached frame would keep
  showing the canvas the flag was just turned off for.

Shading and fog still blend toward what is behind a floating viewport,
which is the page, so the uniform keeps that colour even though nothing
paints it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 00:11:47 +03:00
Hakan SevenandClaude Opus 5 01d8b87db1 fix(solid3d): trim sphere caps and pierced cone legs
Sphere faces drew as complete balls. ACIS parametrises a sphere about
any pole, so a vessel's dished head is commonly bounded by a circle that
is no line of latitude in that frame, and a theta/phi bounding box of
such a boundary covers nearly the whole ball. Derive the window
analytically instead: when a face's loops are circles lying on the
sphere, re-pole the frame onto their common axis, where each seam is an
exact latitude at acos(d / R). One ring bounds a cap, two bound a zone;
the material side comes from the boundary winding, reusing the helper
the torus path already had.

Separately, the longitude wrap flag was resetting the colatitude window
to the whole meridian. A cap centred on the pole covers every longitude
while still ending at its seam, so that reset grew every cap back into a
ball. Trim the two angles independently and rename the flag theta_full
so the split reads at each use, including the silhouette generator,
which skipped the colatitude test the same way.

Cone faces took their height span from the first boundary loop alone. A
lateral face closed off by one rim and one pierce curve -- a pipe branch
meeting its run -- carries its two ends on different loops, so the tube
was sized to whichever came first and the leg above the junction went
undrawn. Read every loop.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 21:44:49 +03:00
Hakan Seven c45daeffb6 feat(render): add scene environment pipeline 2026-08-05 23:42:18 +03:00
Hakan Seven b667eeeb05 refactor(render): unify scene rendering paths
Route model, paper, viewport and nested block content through one traversal while preserving leaf renderers. Carry decoded material state into mesh shading and harden solid geometry conversion.
2026-08-05 21:44:59 +03:00