From 7f6cf38ff4ecdbb201a0ffa472aebafc24ba1c91 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 17:03:48 -0300 Subject: [PATCH 1/7] Bump the version to 0.120.1 A patch: no C symbol was added or removed since v0.120.0, the case v0.52.2 set the precedent for. The behaviour changes that shipped entry points now answer with go in the release notes' "What moves under you" section. --- CMakeLists.txt | 2 +- bindings/c/clay.h | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0657db08..98e92f2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) project(claycore - VERSION 0.120.0 + VERSION 0.120.1 DESCRIPTION "Portable C++20 SDF + voxel engine library" LANGUAGES C CXX) # C: ufbx diff --git a/bindings/c/clay.h b/bindings/c/clay.h index 60ada53a..5ef81111 100644 --- a/bindings/c/clay.h +++ b/bindings/c/clay.h @@ -25,7 +25,7 @@ extern "C" { #define CLAY_ABI_MAJOR 0 #define CLAY_ABI_MINOR 120 -#define CLAY_ABI_PATCH 0 +#define CLAY_ABI_PATCH 1 /* Upper bound on the element count of any batch call: points, rays, cells, * selected node ids, stroke points, polygon vertices. A count above it is diff --git a/pyproject.toml b/pyproject.toml index 1e07ae2a..addae3d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "scikit_build_core.build" [project] name = "pyclay" -version = "0.120.0" +version = "0.120.1" description = "Python bindings for claycore, the portable C++20 SDF + voxel engine library" requires-python = ">=3.9" license = { text = "MIT" } From c2e6fa3fb9cf05071c8b00908133d3aca25e8ed3 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 17:12:32 -0300 Subject: [PATCH 2/7] Add the v0.120.1 release notes Compared against v0.120.0. What moves under a caller: an unconfined infinite grid reports an unbounded tape.bounds and meshing refuses it without a region (#645), bounds narrow per operator (#637), the undo bound reports a grab rather than its node (#648), the node influence bound widens behind a smooth sibling (#653), the brick build keeps a grab the cull cannot judge (#652), voxel sculpt-layer operations and creation are undo steps (#647, #651), and pyclay's multires Layer brushes gain layer_height (#636, #646). The ABI section is diffed against the tag: zero symbols added or removed, no '-' line inside a typedef struct. The device gate result is a marked placeholder until the iOS 27.0 same-OS run lands. --- docs/release-notes/v0.120.1.md | 507 +++++++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 docs/release-notes/v0.120.1.md diff --git a/docs/release-notes/v0.120.1.md b/docs/release-notes/v0.120.1.md new file mode 100644 index 00000000..42bcaf91 --- /dev/null +++ b/docs/release-notes/v0.120.1.md @@ -0,0 +1,507 @@ +# claycore v0.120.1 + +A patch release. Compared against **v0.120.0** (published 2026-09-18), the +previous and latest published release. It covers the eleven PRs merged since +that tag (#635–#638, #645–#648, #651–#653) and the version bump. **No C ABI +symbol was added or removed, and no struct was re-laid out** (see "The ABI"). + +**Read this if your document has an INFINITE REPEAT GRID, if you drive UNDO on +voxel sculpt layers or count undo steps, if you refill bricks from +`clay_document_undo_bound` / `_redo_bound` or `clay_layer_node_influence_bound`, +if you size meshes by `resolution=`, or if you call pyclay's multires Layer +brush.** Every change below reaches a caller through an entry point it already +calls. Nothing announces them: no version number moved for any of them beyond +the patch digit. + +Unless a line says otherwise, every number here is a count or a byte comparison +taken on a Mac from a `cpu-only` build. Timings name their host where the PR or +proposal names one, and say so where it does not. **None of them is a device +number.** + +--- + +## An infinite repeat grid reports an unbounded `tape.bounds` (#645, issue #640) + +`tape.bounds` is where the field can hold material: what the mesher marches, +what a raycast clips against, and what `clay_tape_info` tells a host to plan +bricks over. For an item repeated on an **infinite grid** it reported one cell, +although the copies fill space. On main just before the fix (`9616286c`), a 0.4 +half-size box on a 1.5 grid minus a radius-1 sphere, sampled on 32³ points over +five cells either way: **5,824 of 5,824 material samples fell outside the +reported box**. The sphere removes cell zero completely, so the box held no +material at all. Every entry point that compiles a tape (`compile_document`, +`_part`, `_except`, `compile_layer`, `_prefix`, `compile_item`, and a resumed +`compile_document_append`) reported the same. + +Now, wherever an infinite grid reaches the result **unconfined** — alone, in a +union, or with something subtracted from it — `clay_tape_info` reports +`-FLT_MAX` / `+FLT_MAX` on every axis, as it already did for a plane. A lattice +intersected with something finite is bounded by that thing, and a finite shape +minus a lattice keeps the shape's box. The box is infinite rather than capped, +because any finite box is wrong for a field with material in every cell. + +What a lattice document gives up, because every consumer already treated an +infinite box the way it treats a plane: + +- `clay_mesh`, the quad mesher, `clay_voxel_rasterize`, the bakes and pyclay's + `Volume.from_document` / `moved_topologically_from` / `flattened_from` + **refuse** with "unbounded scene; pass a region" (pyclay: "pass bounds="). + Before, they quietly worked on one cell. +- **`clay_sdf_smooth_begin` refuses the layer**, as it refuses a plane's. Before, + it laid its working lattice over cell zero and left every other copy out of + the transaction. +- Raycast (CPU, Metal, CUDA) and pick skip the AABB clip, which had been + removing hits on every other copy; pick also skips the per-ray local tape. +- `advised_params` gives no consolidation advice, and the SDF prefix cache + declines, so the layer is walked in full. + +A random-document probe linked against each tree found 1,269,305 of 1,419,282 +material samples outside the box on main (518 lattice documents) and **0** on +the fix, across every entry point; documents with no infinite grid planned +byte-identical brick sets (5,942,131 and 5,854,037 bricks over 1,000 documents, +and the nine gallery documents bit-identical). The PR names no host model; the +build was macOS (`cpython-311-darwin`), `cpu-only`. + +## Bounds narrowed per operator (#637) + +`tape.bounds` used to be the plain union of item bounds. It is now folded by +one rule at every level (item, group, layer fold, and a resumed append): a +**subtract keeps its left operand's extent**, an **intersect keeps the +overlap** (or the left operand when gated or when the boxes are disjoint), and +everything else unions with the right operand dilated by its blend ring. **A +smooth group now adds its own blend ring**, which it did not before, so a box +can also grow. No safe step changed. + +Measured on an **Apple M2 Max, Release**, median of 200 meshings at voxel 0.02: +an r0.5 sphere carved by an r2.0 cutter marched **9,640,000 → 125,000 cells** +and meshed **42.18 → 5.26 ms**; two unit spheres intersected, 14.30 → 9.63 ms; +the union control unchanged (37.97 → 38.00 ms). Triangle counts agree within +0.1%. + +**A mesh sized by `resolution=` gets finer on the same resolution**, because +pyclay's `Document.mesh(resolution=)` counts cells along the longest side of +`tape.bounds`. The gallery's `37_groups` went 12,418 → 28,846 triangles at +resolution 88 and **746 KiB against a 400 KiB gallery budget**; it now asks for +58 (12,534 triangles, 324 KiB). `30_trim_curve` went 232 → 7,574 at resolution +72. At a fixed voxel size nothing was lost: 202,624 → 203,020 triangles on +`37_groups` at 0.02, same extent. + +## Undo reports the grab, not its node (#648, issue #639) + +A host Move is one grab per segment at the head of a node's chain, and +`clay_document_undo_bound` / `_redo_bound` reported the union of what each +command targets — for a grab, its node. Every grab widens that node's bound by +its pull, so undoing one segment cost bricks-in-the-node × chain length. +ClaySpaceDesktop measured it against v0.120.0 in a live session: undo wall time +**20 ms → 4,536 ms** over 50 undo steps at a flat ~283k triangles (the host's +own numbers, host machine not stated). + +Now, when the chains before and after a `SetDeformersCmd` differ only in a +**head** of grab, magnify, blob or alpha links — links that are exactly the +identity outside their balls — the step reports those balls (a grab's at its +centre AND its displaced end), placed through every mirror, radial copy, +repetition and instancing layer, dilated by each enclosing group's blend +support and every layer fold above, and **clamped into** the node's bound, so it +is never larger than before. Clamped, not intersected: a ball just past the +node's face still reports that face, because the band a consumer adds can reach +it. + +Radius-1.5 sphere with N grabs around its equator, one radius-0.15 grab on the +pole undone, voxel 0.05, 8³ bricks, C-ABI probe: + +| grabs in the chain | bricks, main before the fix | bricks now | undo p50, before → now | +|---:|---:|---:|---:| +| 1 | 1,000 | **12** | 1.665 → 0.089 ms | +| 10 | 1,440 | **12** | 2.481 → 0.086 ms | +| 40 | 4,000 | **12** | 7.415 → 0.115 ms | + +Timings: **Mac (M-series, model not named), CPU backend, `cpu-only` Release**, +200 undos per point after five discarded, the two builds interleaved. The +brick count is what the tests pin; the clock is not the claim. **The per-brick +price still grows with the chain**: the same 12 bricks cost 7.4 µs each at 1 +grab and 28.1 µs at 160 (same host), though none of those grabs reaches them. +This removes the node-extent factor, not the chain-length one. + +## An edit is bounded by the smooth combines after it (#653, issue #650) + +`node_reach_bound` dilated a node's bound only by its enclosing groups. But a +node is the running value wherever it is the nearest thing, and a **later** +smooth sibling reads the running value out to its support and carries the +change back into the band. Two r = 0.3 spheres 0.3 apart, the second smooth at +k = 0.3, the first moved 0.1: on main before the fix, **6,359 band samples moved +outside the box plus the band, worst 0.0438, and a cache dirtied by +`clay_brick_cache_mark_dirty_nodes` kept 25 stale bricks**. Now 0 and 0. + +So `clay_layer_node_influence_bound`, `clay_brick_cache_mark_dirty_nodes`, the +command path and the undo bound (through #648's clamp) **widen** by the widest +later smooth sibling's full blend support. Nothing widens for a node followed +only by hard combines — every node appended last (a stroke's dabs) and every +document without a smooth blend is bit-identical. On a 400-trial randomized +oracle, bricks refilled rose **2.5%** (plain) and **2.8%** (rich). A per-chain +memo keeps the walk linear: a drag frontier over 1,428 warps of a 10,000-item +smooth layer 0.39 → 0.45 ms, and undoing a 2,134-warp Move 0.85 → 1.04 ms (the +PR and proposal name no host). + +## A full brick build matches the raw field where the deformer cull cannot judge (#652, part of issue #649) + +`Compiler::cull_deformers` dropped grabs that reach a brick in two cases: a +**repeated item**, whose chain runs on the point folded into its repetition +cell while the cull tested the unfolded one (so a brick over any copy except +the source cell was filled undeformed); and a **grab behind another point +warp** (magnify, twist, pose), which moved the point without widening the +region. A repeated item now keeps its whole chain, and the first non-grab link +ends the test. On the issue's probe, rich random documents with a brick build +off the raw field went **62 → 35, worst 0.2632 → 0.0825**; plain documents +(no repeats) are unchanged at 21, worst 0.0310. **#649 is not closed** — see +Known limits. The per-brick cost of keeping a repeated item's whole chain was +not measured. + +## Voxel sculpt layers are on the undo history (#647, #651, issue #642) + +**Five voxel sculpt-layer operations are now undo steps** (#647): +`clay_voxel_set_sculpt_layer_strength`, `_visible`, +`clay_voxel_move_sculpt_layer`, `clay_voxel_remove_sculpt_layer` and +`clay_voxel_merge_sculpt_layer_down`. Through v0.120.0 none recorded anything, +and an undo after a dial reverted the older pass onto cells the dial had +already moved while the slider stayed at the new value. Each step now carries +the property AND every cell the recompose rewrote, and replays without +recomposing, so undo is bit-exact. `clay.h` used to list removing a sculpt +layer and merging one down as operations that destroy history; they no longer +are. + +**Creating a voxel sculpt layer, and every edit made while one records, are +steps** (#651). On main just before the fix (with #647 in), through the C ABI: begin a layer, +one inflate, undo — the layer still listed **110** cells, and dialling it to 0.5 +put 54 undone cells back (2,251 occupied against 2,197); a journal replayed onto +a snapshot older than the layer was refused after 2 of 3 events. Now 0 cells, +2,197 against 2,197, identical document bytes, and 5 of 5 events applied. **A +pass inside a new layer now takes two undos** (the edit, then the layer). +Undoing past the begin ENDS the recording; redoing it brings the layer back +CLOSED, so a host mid-pass that undoes past the begin must begin again. + +Two group-bracket defects fixed with them (#647): + +- **An unmatched `clay_document_end_undo_group` is a no-op.** It used to fold + every step since the session began into one — reached by enabling undo + mid-gesture, where the gesture's begin was refused while undo was off. +- **A nested `begin_undo_group` no longer moves the outer group's fold start.** + A nested group undid in two steps; groups are now depth-counted and only the + outermost folds. + +**Enabling undo mid-session starts an EMPTY history and is never refused.** +That was already the behaviour — `clay_document_enable_undo` builds its history +on first call — and #647 decided to keep it and document it rather than refuse +or record a barrier. What is new is the documentation, and the advice that +comes with #641: the crash journal a mid-session enable starts is paired with +the snapshot the document was last loaded from or saved to, so **save once +right after enabling mid-session**. + +## Multires: the Layer brush in pyclay, the rim, and a stale rebind (#636, #646, #638) + +- **pyclay's multires Layer brushes had a zero layer height and moved nothing.** + `MultiresSculptor.stamp("layer", ...)` (#636, issue #627) and + `SculptLayerStroke.stamp("layer", ...)` (#646, issue #628) now take + `layer_height`, default 0.05 (the engine's), as the last keyword. On a 6x6 + plane at level 1: `layer` moved **0** classes where `draw` moved **37**; both + now move 37. The C entry points never had the gap: `read_mesh_brush` turns a + zero `layer_height` into the default. +- **The layered smooths averaged a rim vertex over half its ring** (#646). On a + regionally refined hierarchy, `LayeredMultiresSculptor::smooth` in + `DetailOnly` and `PreserveDetail` took a rim vertex's neighbours from the + level's own adjacency, which stops at the rim. On a 6x6 cage with the middle + 2x2 at level 3, one dab of radius 0.9: **64 of 64** rim vertices were off the + dense hierarchy (worst 0.00347 and 0.0196); now 0. The code was unchanged + from v0.120.0 up to this fix, so v0.120.0 has it. Uniform hierarchies and + vertices off the rim are unchanged. +- **A multires sculptor rebinds after a restructure** (#636). Its fast path kept + a stroke's records whenever the sculpt level's NUMBER was unchanged, but they + are indexed by vertex. Removing the top level and refining level 3 over a + different region (289 → 81 vertices) read the old record against the new + numbering: 0.078 off a fresh stroke against a 0.08 ceiling. **This is + reachable through the C ABI** with `clay_multires_remove_highest_level` + + `clay_multires_add_level_for_patches`. Separately, `set_base_mesh` under a live + sculptor restarted `cache_generation` onto the value the sculptor last saw, so + it did not rebind and kept stamping through a freed level mesh — a + use-after-free, **C++ only** (no binding exposes `set_base_mesh`). A + process-wide `MultiresSurface::structure_revision()` now decides the rebind. +- **The regional boundary frame is gated** (#638, tests and docs only). The + frame defect the ROADMAP described was fixed on 2026-09-07 (`f40ee3fe`, #491) + and first shipped in v0.97.0; nothing gated it. Three cases now compare + a regional hierarchy with authored rim detail against the dense one. + +## Housekeeping (#635) + +Eighteen finished OpenSpec changes archived and the ROADMAP brought to +v0.120.0; no code change. It filed #629–#634 (see Known limits) and updated the +device-gate skill for the iOS 27.0 iPad. + +--- + +## What moves under you + +### The formats + +**`.clayspace` and the scene format do not move**; `kSceneMinor` is unchanged +and nothing written by v0.120.0 reads differently. **No kernel math changed** — +nothing under `include/clay/kernel/`, `include/clay/eval/` or `backends/` moved +since v0.120.0 — so the kernels artifact's parity fixture is unchanged and a +host pinning `claycore-kernels.zip` re-baselines nothing. + +**The crash journal gains an event** (#647): a voxel sculpt-layer operation is +journaled as a new event kind appended at the end of the enum, with its own +version byte (`kOpVersion = 1`) and `pass_afters` (#651). Older journals still +decode. The journal is session-private and carries no forward-compatibility +promise: **a journal written by v0.120.1 that records a sculpt-layer operation +is not replayable by v0.120.0.** Recover with the library that wrote it. + +### The entry points + +- **`clay_tape_info` returns ±FLT_MAX bounds** for a document where an infinite + grid reaches the result unconfined, and `clay_mesh`, `clay_voxel_rasterize`, + the bakes, pyclay's three `Volume` constructors and `clay_sdf_smooth_begin` + **refuse** such a document or layer without a region (#645). Raycast and pick + now hit every copy. +- **`clay_tape_info`'s box narrows under subtract and intersect and grows by a + smooth group's ring** (#637). Anything sized from it moves: pyclay + `Document.mesh(resolution=)` meshes finer wherever the box narrowed. +- **`clay_document_undo_bound` / `_redo_bound` report a much smaller box** for a + step that adds, removes or changes a head of grab / magnify / blob / alpha + links: 12 bricks where it was 1,000 / 1,440 / 4,000 at 1 / 10 / 40 grabs + (#648). Everything else reports what it did. +- **`clay_layer_node_influence_bound` and `clay_brick_cache_mark_dirty_nodes` + report a wider box** for a node followed by a smooth sibling, by that + sibling's blend support (#653). Nodes followed only by hard combines are + bit-identical. +- **A full brick build fills differently** for a repeated item carrying a grab, + and for a grab behind a magnify, twist or pose (#652): it now matches the raw + field there. +- **Five voxel sculpt-layer operations, the creation of a sculpt layer, and + every edit inside one are undo steps** (#647, #651). `clay_document_undo` + reverses the operation rather than an older step, and `undo_depth` counts + them. **An unmatched `clay_document_end_undo_group` is a no-op**, and **a + nested group folds once**, at the outermost. +- **pyclay `MultiresSculptor.stamp("layer")` and `SculptLayerStroke.stamp("layer")` + move the surface**, up to 0.05 by default (#636, #646). +- **`LayeredMultiresSculptor::smooth` answers differently at the rim of a + regionally refined hierarchy** (#646): it now agrees with the dense hierarchy. +- **C++ only.** `VoxelGrid::begin_sculpt_layer`, `set_sculpt_layer_strength`, + `_visible`, `move_sculpt_layer`, `remove_sculpt_layer` and + `merge_sculpt_layer_down` gain a trailing `SculptLayerOp* record = nullptr` + (source-compatible, not link-compatible); `session::Step::Kind` gains + `VoxelLayerProperty`, so an exhaustive `switch` over it needs a case; + `MultiresSurface::structure_revision()` is new. Recompile. + +--- + +## The ABI, diffed rather than summarised + +`CLAY_ABI_MAJOR/MINOR/PATCH` = 0/120/1. + +`comm` over `clay_*(` in `bindings/c/clay.h` at `v0.120.0` and at `7f6cf38f`: +708 symbols both sides, **zero added, zero removed.** + +`git diff v0.120.0..7f6cf38f -- bindings/c/clay.h` is 89 insertions and 7 +deletions in six hunks. The seven `-` lines are the `CLAY_ABI_PATCH` define +and six comment lines in the undo block and the `clay_tape_info` block. **No `-` +line falls inside a `typedef struct`**, and no `+` line adds a field, a struct, +an enum value or a declaration: every added line but the `CLAY_ABI_PATCH 1` +define is a comment. The comments +state what moved above — the voxel sculpt-layer steps and their creation, the +mid-session enable and the unmatched end, the deformer-head undo bound and what +it does not narrow, the smooth-sibling dilation of +`clay_layer_node_influence_bound`, and the unbounded `clay_tape_info` box. + +--- + +## The gates behind this release + +**The iPad device gate is REGRESSION-capable again.** It is running against +`7f6cf38f` on the reference **iPad15,5 (iPad Air 13-inch, M3), iOS 27.0 +(24A437)**. v0.120.0's record (`tests/device/last-gate.json`: `704f2d4a`, +75 cases, passed, `treeDirty: false`) was the run that **re-baselined** the +suite onto iOS 27.0 in #625, and a baseline written from a run is compared +against itself — so v0.120.0 shipped with `BUDGET` and `GROWTH` coverage and +**no `REGRESSION` coverage**. This is the first same-OS comparison since. + + + +**Device gate: PENDING — result not yet recorded.** + + +**Issue #634's watch item is attributable by this gate.** `stroke_build` went +0.298 → 0.488 ms (1.64x, per-case median across points) between the iOS 26.5.2 +and 27.0 runs, the only case of 74 shared above both the 1.4 tolerance and the +0.125 ms floor, while the median ratio over all 74 was 0.992. A cross-OS ratio +could not say whether iOS or the engine moved. Against the same-OS baseline: +**if `stroke_build` holds near 0.49 ms, it was the OS**, the baseline already +carries it, and #634 closes; **if it rises again, it is the engine**, and the +bisect is between `704f2d4a` and `7f6cf38f`. The case appends quadratic-blended +spheres (k 0.06) through `clay_layer_add_item` and evaluates after each stroke, +so the range's changes to how a compile computes its box (#637, #645) and to +node reach (#653) are where that bisect would start. What this gate cannot do +is split the ORIGINAL 26.5.2 → 27.0 step: that rests on #634's argument that +the timed path did not change across it. + +**No device case measures anything this release changed.** The device suite +has no undo-on/undo-off A/B for a voxel verb (#644), no multires Layer brush, +and no lattice document; `stroke_carve`, which subtracts, is the case #637's +narrowing could reach. A faster reading on it is a candidate for #637, not a +measured claim. + +**The four manual hardware gates are carried as waivers**, recorded at +`59e42ccf` (2026-09-18) with the RTX 5060 runs at `0c3a392c` kept as +`prior_run`. They stay valid because nothing they depend on moved: +`git diff 59e42ccf..7f6cf38f -- include/clay/kernel include/clay/eval backends +cmake/ClayCudaArch.cmake tools/amalgamate_cl.py tools/amalgamate_glsl.py +tests/unit/test_parity.cpp` is **empty**. This release therefore does not claim +that CUDA, OpenCL or Vulkan ran against its tree; the `parity` row compares cpu. + +--- + +## Known limits, stated rather than discovered + +### From this release + +- **#649 is open: a full brick build still disagrees with its own raw field + inside the band** on some documents, and a Move leaves those bricks stale. + #652 fixed the deformer cull; two mechanisms remain. (A) The chain cull pad is + below one blend's support on short chains — the #335 envelope was measured + only at N ≥ 75 and is applied to smaller layers; one seed reaches 0.031, and + the error can reach about 0.1k. Repairing it trades against #335's perf win + and needs its own change. (B) Squashed (non-uniform per-axis scale) + placements are culled as though their field were a distance. After #652 the + rich sweep still has 35 documents off, worst 0.0825. +- **Undo's per-brick cost still grows with chain length** (#648): 7.4 → 28.1 µs + per brick from 1 to 160 grabs (Mac M-series, CPU backend). And these still + report the node's bound: a radial pose, a head behind a twist or any other + whole-item link, an easing not exactly zero at its rim on every backend (the + sines, `out_expo`, the circs, `in_bounce`, `in_out_bounce`), an infinite + repeat grid, a morph or hidden group above the item. +- **Refilling a baked volume** (~430 µs against ~7 µs per brick, the host's + numbers) is #639's second route and was not attempted. +- **A GATED node after the edited one is not modelled** by #653's dilation. A + lerp of two beyond-band values need not stay beyond band; the intersect + surface delta already refuses that case. +- **`clay_layer_bounds` / `pick::selection_bounds` still report one cell for a + lattice** (#645). That box promises a camera frame, not material coverage, + and is left alone. +- **#641: a journal started by a mid-session enable pairs with a stale + snapshot** and replays without the pre-enable edits (probed: replay reports + `{'applied': 1}` and the recovered cell reads 0 where the live document has + 1). `docs/05` and `clay.h` tell hosts to save once after enabling. +- **#643: a `clay_mesh_sculptor` stamp on a document mesh layer is not a + document undo step.** The c-abi scenario that said otherwise was false and now + says what the ABI records. +- **#644: the journal's write-path cost on the device is unmeasured.** The only + figures are Linux x86_64 ratios from `unify-the-undo-history` 1.7 — 1.005x on + `sculpt_smooth`, 1.77x on a bare loop of 64 raw `set` calls, 16 bytes per + journaled cell — and they are not device numbers. +- **Still not an undo step:** creating a MASK, and dropping a resolution level. + A voxel sculpt layer's RECORDING state is not saved, and + `end_sculpt_layer` is not a step. +- **`correct-the-undo-scope` has two open tasks**, and + `make-voxel-sculpt-layer-creation-a-step` (11 of 11 ticked, #651) is not + archived. +- **The mixed-depth multires export has no C, pyclay or Swift entry point** + (`expose-the-mixed-depth-export`, 0 of 8). The C++ export is finished and + gated; neither known host refines a region. +- **Complexity flagged, not refactored:** `History::apply_step` 68 → 71 and + `History::replay` 155 → 158 (#647), and `compile_list` at 59 (#637), all + per-kind switches or already over on v0.120.0. +- **Hosts not named.** #636, #638, #645, #646, #647, #651, #652 report counts + and byte comparisons from `cpu-only` builds; #653's timings name no host; + #648 names "Mac (M-series)"; #637 names an Apple M2 Max. This document + repeats what each says and invents nothing. + +### Unchanged from v0.120.0 + +- **The mesh Grab change (#624) has no device measurement**; the mesh brushes + are not in the device suite. +- **A fixed mesh has no mitigation for a stretched captured region** (a 1.5 + pull: longest edge 0.1174 → 0.8176), **the adaptive surface reaches past the + chord on a curve** (115.79%), **the carried region has no upper bound** (45 → + 811 entries on a 1.5 pull-out), and **the collapse protection is measured only + on sphere grab strokes**. +- **There is no faithful SDF Standard**; building one needs its own proposal — + now filed as #633. **ClayBuildup is unmeasured.** **Relief's stepping bound + is poor at stroke density** (L = 46 over 30 dabs), and per-dab + `move_surface`'s safe step collapses by 1.5x per dab (#632). +- **Adaptive undo is last-in first-out only and nothing compacts**; + **`serialize` is not the test for "undo restored the surface"**; **a second + sculptor over the same surface is not kept in step**; **`bytes()` is a + budget, not an assertion**; **a recorded stroke accumulates into its + record**. +- **The C++ `session::History` path still leaves a held `DynamicSculptor`'s + index stale** after an undo — now filed as #629 (200 of 6,912 live faces + unindexed). +- **The adaptive stroke calls are not in the device coverage gate.** +- **#531 is not closed and the all-brush 16 ms target is unmet.** Nothing in + this release claims otherwise. +- **#621 has no uncontended timing; native GPU execution of #613/#616/#621/#623 + was not measured; #616's p95 is a small-sample statistic.** +- **Decimation can still emit a non-manifold mesh at some ratios on some + toolchains** (#575), and **a mesh is not byte-identical across toolchains.** +- **Regional consolidation's local path is narrow by construction.** +- **`clay_stroke_preset`'s `rotate_to_azimuth` and `velocity_*` controls read + channels only `clay_stroke_sample_full` carries**, and **the flat-packed mesh + and multires stroke calls still take `count*5` floats.** +- **Move still builds no stroke preset**, and **the SDF Move brush still moves + less than the displacement asked for.** +- **"`clay_list_backends` reports metal on a real iPad" has not been observed + by this repository** (#45), and **`check_gallery.py` gates a manifest, not an + image.** + +### Changed from v0.120.0 + +- **"No `REGRESSION` coverage" (the iOS 27.0 re-baseline) is closed by this + release's device gate — IF it passes.** See the placeholder above; if the gate + fails or cannot be scored, this line moves back to "Unchanged". +- **"The four manual hardware gates are stale"** — v0.120.0 answered them as + waivers at `59e42ccf`, and they remain valid here; see "The gates". + +--- + +## Upgrading + +Ordered by how much risk each item carries. + +1. **If a document can hold an infinite repeat grid, test `clay_tape_info`'s + box for ±FLT_MAX and pass a region** to `clay_mesh`, + `clay_voxel_rasterize`, the bakes and pyclay's `Volume` constructors + (`bounds=`), or they now refuse (#645). Plan bricks over the view, not the + box. `clay_sdf_smooth_begin` refuses such a layer outright. +2. **Recount undo steps on voxel sculpt layers** (#647, #651). Strength, + visibility, move, remove, merge-down, layer creation and every edit inside a + layer are each a step; a pass in a new layer is two undos. A host mid-pass + that undoes past the begin must begin the layer again. An unmatched + `clay_document_end_undo_group` no longer folds the history, and a nested + group folds once — remove any compensation for either. +3. **If you enable undo mid-session, save once right after** (#641). +4. **Re-pick `resolution=` for resolution-sized meshes** (#637). A subtract or + intersect now meshes finer at the same resolution, a smooth group's ring can + make it coarser; at a fixed voxel size nothing changes. +5. **If you dirty bricks from `clay_document_undo_bound` / `_redo_bound` or + `clay_layer_node_influence_bound`, take the new boxes as they come** + (#648, #653). The undo box is smaller for Move segments; the node box is + wider behind a smooth sibling. Do not pad or clip them: the first is proven + by the identity outside the balls, the second fixes stale bricks. +6. **Re-record any golden that meshes or brick-builds a repeated item with a + grab, or a grab behind a magnify / twist / pose** (#652). The field there is + now the raw field. +7. **pyclay scripts calling the multires Layer brush now move the surface** + (#636, #646), up to 0.05 by default. Pass `layer_height=` to set it. +8. **C++ callers recompile** (`VoxelGrid` out-parameters, `Step::Kind`), and a + `switch` over `Step::Kind` gains `VoxelLayerProperty`. +9. **Do not replay a v0.120.1 crash journal with v0.120.0** if it recorded a + sculpt-layer operation. +10. **Nothing to do for `.clayspace`, the scene format, the kernels artifact or + the C ABI.** No symbol, signature, layout or format minor changed. From 2383223c8cab4fa54c0390bd2f4d639b0d746def Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 17:12:32 -0300 Subject: [PATCH 3/7] Record 0.120.1 in the RELEASE.md history A patch with no ABI surface change that a caller still observes in seven ways, each with its measurement, and the eleven merges that carried the released 0.120.0 line without being in v0.120.0. --- docs/RELEASE.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/RELEASE.md b/docs/RELEASE.md index d7cc3332..f4a8658c 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1054,6 +1054,82 @@ forward-refuse). v0.116.0 tag and its first commit is the bump, so the gap that produced the 0.78.0 and 0.103.0 stragglers below did not open this time. + **0.120.1 is a patch: no symbol added or removed and no struct re-laid out, + AND IT IS A RELEASE A CALLER OBSERVES, in seven ways, none announced by any + version gate.** Against v0.120.0: 708 `clay_*(` symbols both sides, and the + seven `-` lines in the `clay.h` diff are the `CLAY_ABI_PATCH` define and six + comment lines, none inside a `typedef struct`. Every added line but the + define is a comment. No kernel, `.clayspace`, scene or kernels-fixture + change. + + **An unconfined infinite repeat grid reports an unbounded `tape.bounds`** + (#645 -- issue #640). It reported one cell, although the copies fill space: + a 0.4 box on a 1.5 grid minus a radius-1 sphere had **5,824 of 5,824** + material samples outside the box, measured on main at `9616286c`. Now + `clay_tape_info` returns +/-FLT_MAX as for a plane, and `clay_mesh`, + `clay_voxel_rasterize`, the bakes, pyclay's `Volume` constructors and + `clay_sdf_smooth_begin` REFUSE such a document without a region where they + used to work on one cell. A random-document probe found 1,269,305 samples + outside on main and 0 after; documents with no infinite grid plan + byte-identical bricks. + + **`tape.bounds` narrows per operator** (#637): a subtract keeps its left + operand, an intersect the overlap, and a smooth group now adds its own blend + ring. A carve by a large cutter marched **9,640,000 -> 125,000 cells** and + meshed 42.18 -> 5.26 ms (Apple M2 Max, Release, median of 200). A mesh sized + by pyclay's `resolution=` meshes finer at the same resolution: the gallery's + `37_groups` went 12,418 -> 28,846 triangles and **746 KiB against its 400 KiB + budget** at resolution 88, and now asks for 58. Nothing changes at a fixed + voxel size. + + **`clay_document_undo_bound` / `_redo_bound` report a grab, not its node** + (#648 -- issue #639). A step whose chains differ only in a head of grab, + magnify, blob or alpha links reports their balls, placed and dilated as the + item is, and clamped into the node's bound: **12 bricks where it was 1,000 / + 1,440 / 4,000 at 1 / 10 / 40 grabs**, undo p50 1.665 -> 0.089 ms at one grab + (Mac M-series, CPU backend, `cpu-only` Release). The per-brick price still + grows with the chain, **7.4 -> 28.1 us from 1 to 160 grabs**. + + **`clay_layer_node_influence_bound` and `clay_brick_cache_mark_dirty_nodes` + widen behind a smooth sibling** (#653 -- issue #650), by its full blend + support: a later smooth combine carries an edit back into the band. Two + r = 0.3 spheres blended at k = 0.3, the first moved 0.1: **6,359 band samples + outside the bound and 25 stale bricks before, 0 and 0 after**; a randomized + oracle refills 2.5% and 2.8% more bricks. A node followed only by hard + combines, including every node appended last, is bit-identical. + + **A full brick build keeps a grab the deformer cull cannot judge** (#652, + part of issue #649): on a repeated item, and behind a magnify, twist or pose. + Rich random documents with a build off the raw field went **62 -> 35, worst + 0.2632 -> 0.0825**. #649 stays open on two other mechanisms. + + **Voxel sculpt-layer operations are undo steps** (#647 and #651 -- issue + #642). Strength, visibility, move, remove and merge-down record one each, as + do a layer's creation and every edit inside a recording layer; through + 0.120.0 none did, and undoing a pass left its **110** cells in the layer's + record for the next dial to put back. A pass in a new layer is now two undos. + **An unmatched `clay_document_end_undo_group` is a no-op** -- it folded every + step since the session began into one -- and **a nested group folds once**. + Enabling undo mid-session starts an empty history, as it always did; that is + now documented, with #641's advice to save right after. The crash journal + gains a session-private event kind, so a v0.120.1 journal carrying one is not + replayable by v0.120.0. + + **pyclay's multires Layer brushes move the surface** (#636, #646 -- issues + #627, #628): `MultiresSculptor.stamp` and `SculptLayerStroke.stamp` take + `layer_height`, default 0.05, where a zero ceiling moved **0 classes against + Draw's 37**. The layered smooths average a rim vertex over its whole ring + (**64 of 64** rim vertices were off the dense hierarchy), and a multires + sculptor rebinds after a restructure (reachable through + `clay_multires_remove_highest_level` + `_add_level_for_patches`) and after a + C++-only `set_base_mesh` that had left it stamping through a freed level. + #635 and #638 are docs, OpenSpec and tests only. + + **All eleven merges carry the already-released 0.120.0 version line and are + NOT in v0.120.0**: #635-#638, #645-#648 and #651-#653 merged after the tag + without a bump, and ship here. The bump is the release branch's own commit, + `7f6cf38f`. + **0.113.0 stops decimation breaking a manifold it was given** (#567). meshoptimizer chooses its own collapses and does not apply the link condition collapse_edge refuses on, so clay_document_mesh -- documented as the From 71ea457834253b9f66dc8f9235c4e9c2e8af5b17 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 17:14:44 -0300 Subject: [PATCH 4/7] Correct v0.120.0's gate record, archive the voxel layer-creation change v0.120.0's notes said its hardware and device gates were stale: that section was written before the gates ran and not updated before the tag. As tagged, the four hardware gates were waived at 59e42ccf and the device gate passed at 704f2d4a on iOS 27.0 -- the re-baseline run, which is why v0.120.0 had no regression coverage. Corrected with a dated note above the original text, which is kept. make-voxel-sculpt-layer-creation-a-step (#651) had all 11 tasks ticked and was not archived by the PR that finished it; archived here, with its scene-model delta synced. The roadmap header is recounted: 21 capabilities, 240 archived, 41 open. The v0.120.1 notes now say why this is a patch although pyclay gained a keyword argument, citing the 0.24.2 precedent. --- docs/release-notes/v0.120.0.md | 26 ++++++++++++++++ docs/release-notes/v0.120.1.md | 9 ++++++ openspec/ROADMAP.md | 3 +- .../design.md | 0 .../proposal.md | 0 .../specs/scene-model/spec.md | 0 .../tasks.md | 0 openspec/specs/scene-model/spec.md | 31 +++++++++++++++++++ 8 files changed, 68 insertions(+), 1 deletion(-) rename openspec/changes/{make-voxel-sculpt-layer-creation-a-step => archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step}/design.md (100%) rename openspec/changes/{make-voxel-sculpt-layer-creation-a-step => archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step}/proposal.md (100%) rename openspec/changes/{make-voxel-sculpt-layer-creation-a-step => archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step}/specs/scene-model/spec.md (100%) rename openspec/changes/{make-voxel-sculpt-layer-creation-a-step => archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step}/tasks.md (100%) diff --git a/docs/release-notes/v0.120.0.md b/docs/release-notes/v0.120.0.md index bf8adde1..8053c6b8 100644 --- a/docs/release-notes/v0.120.0.md +++ b/docs/release-notes/v0.120.0.md @@ -621,6 +621,32 @@ the `struct_size` pattern: ## The gates behind this release +> **CORRECTED 2026-09-23, in v0.120.1's release PR.** This section was written +> before the gates it describes had run, and it was not updated before the tag. +> It said all four hardware gates and the device gate were stale. What the tag +> actually carries is below; the original text follows it, unchanged. + +**As tagged:** + +- **The four hardware gates were WAIVED, not re-run.** `tests/hardware/manual-gates.json` + records all four (`cuda-parity`, `nvcc-build`, `opencl-device`, `vulkan-device`) + as `waived` at `59e42ccf`, with the RTX 5060 run at `0c3a392c` kept as + `prior_run`. The reason: the only kernel-relevant change was #618's two + trailing comments in `include/clay/kernel/tape.h`, enum values unchanged. **No + GPU backend ran against v0.120.0's tree**; the cpu `parity` row covers cpu alone. +- **The device gate PASSED, on a new OS.** `tests/device/last-gate.json` records + claycore `704f2d4a` at ABI 0.120.0 on `iPad15,5` / **iOS 27.0 (24A437)**: 75 + cases, seven cold sessions, thermal state nominal at both ends, + `treeDirty: false`. The reference iPad had moved from iOS 26.5.2, so the + baseline was re-taken from this run (#625). **That means v0.120.0 had no + regression coverage**: a baseline compared against the run it came from + cannot fire `REGRESSION`. `BUDGET` and `GROWTH` still gated. Coverage returns + with v0.120.1, the first same-OS comparison. +- The device host needed the scene lifecycle to launch on the iOS 27 SDK + (`tests/device/Host/`); without it, session 1 died before any test ran. + +**As originally written** (superseded by the above): + **The four manual hardware gates are STALE and must be re-answered before this is tagged.** All four (`cuda-parity`, `nvcc-build`, `opencl-device`, `vulkan-device`) are recorded as **run** on an RTX 5060 host at commit diff --git a/docs/release-notes/v0.120.1.md b/docs/release-notes/v0.120.1.md index 42bcaf91..e00ab11f 100644 --- a/docs/release-notes/v0.120.1.md +++ b/docs/release-notes/v0.120.1.md @@ -313,6 +313,15 @@ it does not narrow, the smooth-sibling dilation of --- + +**Why a patch number, stated rather than assumed.** No C symbol was added or +removed and no struct was re-laid out, which is what moves this repo's minor. +pyclay did gain one keyword argument, `layer_height`, on `MultiresSculptor.stamp` +and `SculptLayerStroke.stamp` (#636, #646). `docs/RELEASE.md` says the Python API +follows SemVer, under which an additive keyword is a minor. This ships it under +a patch, following 0.24.2, which shipped two new symbols under a patch number and +said so in its notes, as this paragraph does. + ## The gates behind this release **The iPad device gate is REGRESSION-capable again.** It is running against diff --git a/openspec/ROADMAP.md b/openspec/ROADMAP.md index f9bde9c1..d8b9efc3 100644 --- a/openspec/ROADMAP.md +++ b/openspec/ROADMAP.md @@ -158,7 +158,8 @@ because none has a proposal yet: ## Where the engine is (2026-09-23, v0.120.0) -21 capabilities, 238 archived changes, 41 still open. v0.120.0 was published +21 capabilities, 240 archived changes, 41 still open (recounted for v0.120.1; +no open change has zero open tasks). v0.120.0 was published 2026-09-18. On 2026-09-23 `unify-the-undo-history` finished — voxel sculpt-layer operations became undo steps, the mid-session enable was decided, and the device half of its measurement was re-scoped to #644 — and archived diff --git a/openspec/changes/make-voxel-sculpt-layer-creation-a-step/design.md b/openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/design.md similarity index 100% rename from openspec/changes/make-voxel-sculpt-layer-creation-a-step/design.md rename to openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/design.md diff --git a/openspec/changes/make-voxel-sculpt-layer-creation-a-step/proposal.md b/openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/proposal.md similarity index 100% rename from openspec/changes/make-voxel-sculpt-layer-creation-a-step/proposal.md rename to openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/proposal.md diff --git a/openspec/changes/make-voxel-sculpt-layer-creation-a-step/specs/scene-model/spec.md b/openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/specs/scene-model/spec.md similarity index 100% rename from openspec/changes/make-voxel-sculpt-layer-creation-a-step/specs/scene-model/spec.md rename to openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/specs/scene-model/spec.md diff --git a/openspec/changes/make-voxel-sculpt-layer-creation-a-step/tasks.md b/openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/tasks.md similarity index 100% rename from openspec/changes/make-voxel-sculpt-layer-creation-a-step/tasks.md rename to openspec/changes/archive/2026-09-23-make-voxel-sculpt-layer-creation-a-step/tasks.md diff --git a/openspec/specs/scene-model/spec.md b/openspec/specs/scene-model/spec.md index 20120c11..ddb528d1 100644 --- a/openspec/specs/scene-model/spec.md +++ b/openspec/specs/scene-model/spec.md @@ -2252,3 +2252,34 @@ Dropping a voxel resolution level destroys the detail it held and is recorded by #### Scenario: The step count matches what can be undone - **WHEN** a host reads the history depth - **THEN** it equals the number of steps that will actually reverse something, so a menu built from it never offers an undo that does nothing + +### Requirement: A voxel sculpt layer's creation and the edits inside it are steps +Creating a voxel sculpt layer SHALL be a history step: undoing it SHALL remove the layer from the stack and end any recording into it, and redoing it SHALL restore the layer, with its name and dither seed, without reopening the recording. + +An edit made while a voxel sculpt layer is recording SHALL be one step that carries both the cells it wrote and what it did to the layer's record of the pass. Undoing it SHALL restore the cells and the record together, so that the grid and its stack are byte-identical to their state before the edit and a later change to the layer's strength or visibility SHALL NOT re-apply the undone cells. + +A journal replayed onto a snapshot taken before a voxel sculpt layer existed SHALL rebuild the layer and its passes, and SHALL NOT be refused at an operation that names it. + +An edit inside a voxel sculpt layer that changed no cell SHALL NOT be a step, and SHALL leave the layer's record as it found it. + +#### Scenario: Undoing an edit takes it out of the layer +- **GIVEN** a document with undo enabled and a voxel sculpt layer recording +- **WHEN** an inflate is made inside the layer and the history is undone once +- **THEN** the layer's cell count is zero and the document is byte-identical to its state before the inflate +- **AND** setting the layer's strength to 0.5 afterwards leaves the occupied cell count unchanged + +#### Scenario: Undoing a creation removes the layer +- **GIVEN** a document with undo enabled +- **WHEN** a voxel sculpt layer is created, a pass is made inside it, and the history is undone twice +- **THEN** the grid has no sculpt layers and the document is byte-identical to its state before the creation +- **AND** redoing twice returns the layer and its pass byte-identically + +#### Scenario: A journal rebuilds a layer the snapshot lacks +- **GIVEN** a snapshot taken before any voxel sculpt layer existed +- **WHEN** two layers are created with a pass each, one is dialled, and the journal since the snapshot is replayed onto it +- **THEN** every event applies and the recovered document is byte-identical to the live one + +#### Scenario: A no-op edit inside a layer leaves no trace +- **GIVEN** a voxel sculpt layer recording, with undo enabled +- **WHEN** an edit that changes no cell is made inside it +- **THEN** the undo depth does not change and the grid and its stack are byte-identical to their state before the edit From 9120334b3a6bbdada5fb3b02dc6fb11bdd0e3223 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 17:18:43 -0300 Subject: [PATCH 5/7] Summarise everything since v0.120.0 at the top of the v0.120.1 notes One row per PR merged since the last published release, each with the headline number from its own section, which names what it was measured on. No new figure is introduced. --- docs/release-notes/v0.120.1.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/release-notes/v0.120.1.md b/docs/release-notes/v0.120.1.md index e00ab11f..8a42150b 100644 --- a/docs/release-notes/v0.120.1.md +++ b/docs/release-notes/v0.120.1.md @@ -20,6 +20,32 @@ number.** --- +## Everything since v0.120.0, at a glance + +Every PR merged since the last published release, v0.120.0 (2026-09-18). Each +row's number is taken from its section below, which says what it was measured +on. + +| PR | What improved | Measured | +|---|---|---| +| **#648** (issue #639) | **Undoing one Move segment refills the grab, not its whole node.** Undo cost stops scaling with the node's size. | bricks per undo 1,000 / 1,440 / 4,000 → **12** at 1 / 10 / 40 grabs; undo 7.415 → 0.115 ms at 40 (Mac, CPU) | +| **#637** | **Bounds narrowed per operator**: a subtract keeps its left operand's extent, an intersect keeps the overlap. Adds a layer-vs-group parity test through the C ABI; finishes `fold-the-layers-with-an-operator`. | a carved sphere marched 9,640,000 → **125,000** cells, meshed 42.18 → **5.26 ms** (M2 Max) | +| **#645** (issue #640) | **An infinite repeat grid reports unbounded bounds** instead of one cell; meshing, rasterizing, baking and `clay_sdf_smooth_begin` refuse one without a region rather than silently drop material. | material samples outside the box: 5,824 of 5,824 → **0** | +| **#653** (issue #650) | **An edit is bounded by the smooth combines after it**, so a cache dirtied from the reported bound no longer keeps stale bricks at a smooth seam. | stale bricks 25 → **0**; bricks refilled +2.5% / +2.8% on a randomized oracle | +| **#652** (part of issue #649) | **A full brick build matches the raw field** for repeated items and grabs behind another warp. #649 stays open. | rich documents off the raw field 62 → **35**, worst 0.2632 → **0.0825** | +| **#647** (issue #642) | **Five voxel sculpt-layer operations are undo steps** (strength, visibility, move, remove, merge-down), replayed bit-exactly; a nested undo group folds once. | undo → redo → undo byte-identical | +| **#651** (issue #642) | **Creating a voxel sculpt layer, and each edit inside one, are undo steps.** | an undone inflate left **110** cells in the layer; now 0 | +| **#636** (issue #627) | **Multires rebinds after a restructure** (reachable through the C ABI) **and after `set_base_mesh`** (a use-after-free, C++ only); pyclay's multires Layer brush moves again. | Layer brush 0 → **37** classes moved | +| **#646** (issue #628) | **Regional multires smoothing averages a rim vertex over its whole ring**; pyclay's `SculptLayerStroke` Layer brush moves again. | rim vertices off the dense hierarchy 64 of 64 → **0** | +| **#638** | The regional boundary frame is now gated by tests with authored rim detail. Tests and docs only; the fix itself shipped in v0.97.0. | — | +| **#635** | Housekeeping: 18 finished OpenSpec changes archived, the roadmap brought current, issues #629–#634 filed, the device-gate skill updated for iOS 27. | — | + +This release PR itself also corrects v0.120.0's gate record (its notes said the +gates were stale; as tagged, the hardware gates were waived and the device gate +passed on iOS 27.0) and archives #651's finished OpenSpec change. + +--- + ## An infinite repeat grid reports an unbounded `tape.bounds` (#645, issue #640) `tape.bounds` is where the field can hold material: what the mesher marches, From 3c0b27330e7a97daebb666af56e19abecf1f34e5 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 18:41:42 -0300 Subject: [PATCH 6/7] Record the device gate for v0.120.1 iPad15,5 on iOS 27.0 (24A437), claycore 7f6cf38f at ABI 0.120.1: seven cold sessions, 75 cases, thermal state nominal at both ends, tree clean. Scored against the iOS 27.0 baseline taken at v0.120.0 -- the first same-OS comparison since the re-baseline, so REGRESSION could fire this time. Nothing failed: median ratio 0.998 across 75 cases, none above both the 1.4 tolerance and the 0.125 ms floor. The canary drifted x1.65 across the run while thermalState read nominal; sdf_flatten and sdf_relax were measured past x1.3 of it and their budgets held. --- tests/device/last-gate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/device/last-gate.json b/tests/device/last-gate.json index cd60ec80..14f9af5a 100644 --- a/tests/device/last-gate.json +++ b/tests/device/last-gate.json @@ -1,7 +1,7 @@ { - "abiVersion": "0.120.0", + "abiVersion": "0.120.1", "caseCount": 75, - "claycoreCommit": "704f2d4af5a466413374530d824b6c5a16603557", + "claycoreCommit": "7f6cf38ff4ecdbb201a0ffa472aebafc24ba1c91", "deviceModel": "iPad15,5", "osVersion": "Version 27.0 (Build 24A437)", "passed": true, From fe6db52d639eb4e5de55a3933c47b8509061b130 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo dos Santos Date: Wed, 23 Sep 2026 18:42:29 -0300 Subject: [PATCH 7/7] Record the v0.120.1 device gate result in the notes and history The gate passed against the iOS 27.0 baseline: median ratio 0.998 over 75 cases, none above both the tolerance and the floor. stroke_build (#634) read 0.324 ms against the baseline's 0.488 -- it fell rather than held or rose, so the notes' two predictions are replaced with what was measured, and the ambiguity about v0.120.0's high reading is stated. --- docs/RELEASE.md | 9 ++++++ docs/release-notes/v0.120.1.md | 59 ++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/docs/RELEASE.md b/docs/RELEASE.md index f4a8658c..44de55a6 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1082,6 +1082,15 @@ forward-refuse). budget** at resolution 88, and now asks for 58. Nothing changes at a fixed voxel size. + **0.120.1 restores regression coverage on the device.** Its gate is the first + same-OS comparison since the iOS 27.0 re-baseline (#625), which left 0.120.0 + comparing a run against itself. It passed: iPad15,5 on iOS 27.0, 75 cases, + **median ratio 0.998 against the baseline, none above both the 1.4 tolerance + and the 0.125 ms floor**, with the canary drifting x1.65 while thermalState + read nominal. `stroke_build`, #634's watch item, read **0.324 ms against the + baseline's 0.488** (0.66x, within 1.09x of its 26.5.2 figure); #634 is closed + on that, the cause of v0.120.0's high reading left unresolved. + **`clay_document_undo_bound` / `_redo_bound` report a grab, not its node** (#648 -- issue #639). A step whose chains differ only in a head of grab, magnify, blob or alpha links reports their balls, placed and dilated as the diff --git a/docs/release-notes/v0.120.1.md b/docs/release-notes/v0.120.1.md index 8a42150b..ffc8bf9b 100644 --- a/docs/release-notes/v0.120.1.md +++ b/docs/release-notes/v0.120.1.md @@ -359,32 +359,37 @@ against itself — so v0.120.0 shipped with `BUDGET` and `GROWTH` coverage and **no `REGRESSION` coverage**. This is the first same-OS comparison since. - -**Device gate: PENDING — result not yet recorded.** +**Device gate: PASSED, scored against the iOS 27.0 baseline.** Recorded in +`tests/device/last-gate.json`: claycore `7f6cf38f` at ABI 0.120.1, `iPad15,5` +on iOS 27.0 (24A437), 75 cases, seven cold sessions with 900 s cooldowns, +thermal state `nominal` at both ends, `treeDirty: false`. The console showed no +RunningBoard hot-condition or termination line in any session. + +- **No `REGRESSION`, `BUDGET` or `GROWTH` failure.** Taking each case's median + across its points, the median ratio against the baseline is **0.998** over all + 75 cases, and **no case is above both the 1.4 tolerance and the 0.125 ms + floor**. The largest rise is `armature_edit` at 1.23x (0.504 → 0.621 ms). +- **CONDITIONS CHANGED:** the canary drifted **x1.65** across the run (98.93 → + 163.08 ms) while `thermalState` read `nominal` throughout — the case the + canary exists for. `sdf_flatten` (x1.43 of the settled canary) and + `sdf_relax` (x1.33) were measured past x1.3; both budgets held, and against the + baseline they read 1.01x and 1.13x. +- 14 cases needed repeats for an honest p95 (median of 3 passes), led by + `mask_extrude` (spread 294 ms) and `sdf_stamp_detail_bricks` (4.85 ms). -**Issue #634's watch item is attributable by this gate.** `stroke_build` went -0.298 → 0.488 ms (1.64x, per-case median across points) between the iOS 26.5.2 -and 27.0 runs, the only case of 74 shared above both the 1.4 tolerance and the -0.125 ms floor, while the median ratio over all 74 was 0.992. A cross-OS ratio -could not say whether iOS or the engine moved. Against the same-OS baseline: -**if `stroke_build` holds near 0.49 ms, it was the OS**, the baseline already -carries it, and #634 closes; **if it rises again, it is the engine**, and the -bisect is between `704f2d4a` and `7f6cf38f`. The case appends quadratic-blended -spheres (k 0.06) through `clay_layer_add_item` and evaluates after each stroke, -so the range's changes to how a compile computes its box (#637, #645) and to -node reach (#653) are where that bisect would start. What this gate cannot do -is split the ORIGINAL 26.5.2 → 27.0 step: that rests on #634's argument that -the timed path did not change across it. +**Issue #634's watch item is answered, and not the way either prediction +expected.** `stroke_build` read 0.298 ms on iOS 26.5.2 and 0.488 ms at the +v0.120.0 re-baseline on 27.0 (1.64x, per-case median across its points). On the +same OS, this gate reads **0.324 ms — 0.66x the baseline, and within 1.09x of the +26.5.2 figure.** So the 1.64x neither persisted on 27.0 nor was carried by the +OS. Two readings remain and this case cannot separate them: the v0.120.0 run's +reading was high, or something in this range recovered it — #637's narrower +bounds would cut the planning a `clay_layer_add_item` pays, which is what this +case measures. It is a gallery one-shot case: one sample per point, its first +point a cold 21.7 ms against 0.155–0.455 ms for the other seven, an internal +spread near 100x. **#634 is closed by this release** on that evidence, with the +ambiguity stated rather than resolved. **No device case measures anything this release changed.** The device suite has no undo-on/undo-off A/B for a voxel verb (#644), no multires Layer brush, @@ -497,9 +502,9 @@ that CUDA, OpenCL or Vulkan ran against its tree; the `parity` row compares cpu. ### Changed from v0.120.0 -- **"No `REGRESSION` coverage" (the iOS 27.0 re-baseline) is closed by this - release's device gate — IF it passes.** See the placeholder above; if the gate - fails or cannot be scored, this line moves back to "Unchanged". +- **"No `REGRESSION` coverage" (the iOS 27.0 re-baseline) is closed.** This + release's device gate is the first same-OS comparison since #625, and it + passed: median ratio 0.998 over 75 cases, nothing past tolerance and floor. - **"The four manual hardware gates are stale"** — v0.120.0 answered them as waivers at `59e42ccf`, and they remain valid here; see "The gates".