Repository navigation
Undo a multires gesture across the C ABI: clay_multires_delta (#671) - #687
Merged
Merged
Conversation
A clay_multires is a standalone handle no document layer owns, and neither MultiresDelta nor SculptLayerDelta crossed the ABI, so a host's only undo for a committed multires gesture was a whole-hierarchy snapshot. clay_multires_delta holds both halves of a gesture and is captured by clay_multires_sculptor_stamp_recorded / _apply_stroke_recorded and clay_multires_sculpt_layer_stroke_commit_into; the shipped calls are unchanged and share one implementation with a null record. Revert and apply go through the hierarchy, are idempotent, and leave the moved patches in clay_multires_dirty_blocks. mesh::MultiresGesture adds the binding the C++ records lack: they refuse only by entry counts, so a twin hierarchy or a relevelled one accepted a record and was written. The gesture keeps the structure_revision and a per-process origin at capture and refuses anything else with CLAY_ERROR_SNAPSHOT_MISMATCH before writing. Also: apply_to_multires records the active-pass half (it dropped it, so a recorded stroke on a hierarchy with passes was empty), and a pass replay evaluates the display level so its patches are marked. ABI 0.124.0 -> 0.125.0, additive.
Round trips through every way a gesture lands (plain stroke and stamp, level 0, a refined region's rim, a mirrored pair, a sculpt-layer transaction, the plain sculptor on an active pass), the same stroke again after an undo, the twin / relevel / removed-pass refusals, coalescing, the byte formula and spill, and dirty patches after a replay.
leonardoaraujosantos
force-pushed
the
feat/671-multires-delta-abi
branch
from
October 4, 2026 11:36
187952d to
9bfd4a6
Compare
This was referenced Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A C host could not put a multiresolution gesture into its undo stack (#671). The engine records both kinds of gesture —
mesh::MultiresDelta(cage positions, a level's own coefficients) andmesh::SculptLayerDelta(one sculpt pass) — butclay.hsaid twice that neither crossed the ABI, and pointed at pyclay, which does not expose them either. Aclay_multiresis a standalone handle no document layer owns, soclay_document_undocannot reach it, andclay_multires_sculpt_layer_stroke_cancelis exact only before commit. After commit, a host's only undo was a whole-hierarchyclay_multires_serializesnapshot per gesture.What lands
clay_multires_delta, theclay_dynamic_deltashape, so a host keeps one pattern for every sculptable representation:clay_multires_delta_create/_destroy/_clear/_stats_get/_levels/_revert/_apply/_serialize/_deserialize, and theclay_multires_delta_statsdescriptor (struct_sizefirst).clay_multires_sculptor_stamp_recorded,clay_multires_sculptor_apply_stroke_recorded,clay_multires_sculpt_layer_stroke_commit_into. The shipped_stamp,_apply_strokeand_commitare unchanged; each now shares one body with its recorded twin, called with a null record.mesh::MultiresGesture(new) holds both halves, the binding and the envelope;MultiresDelta/SculptLayerDeltagainmatches, split counts andencoded_size;brush::apply_to_multiresgains a trailing, defaultedSculptLayerDelta*.undo-a-multires-gesture-across-the-abi(proposal with measurements, design, tasks, c-abi delta).docs/05anddocs/07§ 8d are updated.What measuring found
revertrefuses only by entry counts. A twin built from the same cage, and the same hierarchy afterremove_highest_level+add_level, both pass that check and were written. The record now carries the hierarchy'sstructure_revisionplus a per-process origin (the counter restarts in every process), and refuses anything else withCLAY_ERROR_SNAPSHOT_MISMATCHbefore writing. With the binding disabled, 8 assertions fail.apply_to_multirespassed only the base record. With an active pass the sculptor writes the pass, so a recorded stroke came back empty. Nulling the layer half fails 7 of 14 cases (31 assertions).clay_multires_dirty_block_countwas 0 while the evaluated revision had moved. A pass replay now evaluates the display level before returning. This answers the issue's "please confirm revert/apply mark those blocks dirty": they do now, for both halves.The encoding is 37–47% of the snapshot here. The resident record is not smaller than the snapshot on this fixture, because it holds slot maps (about 2.2x its encoding). What changes is the scaling: a snapshot carries all the detail sculpted so far, while a record carries one gesture. A host spills records with
_serialize. One stamp and forty stamps on the same spot record identical entry counts and bytes.Design decisions a reviewer should check
structure_revision+ process origin, not a structural fingerprint. A fingerprint would let a record survive relevel-and-back and a decode of the hierarchy's own bytes, but it accepts a twin, which the acceptance criteria refuse. The cost: a record does not survive a level change, even one later undone, nor a snapshot restore into a new handle. This is the same lifetime ruleclay_dynamic_deltaalready states, and it is in the header under WHAT IT DOES NOT PROMISE. For ClaySpaceIOS, undoing past a level change still restores from that change's snapshot, and multires records older than it are then refused. If the host needs those older records to survive, the follow-up is level add/remove records that restore the structure token. That is the issue's optional item, and this PR does not include it.commit_intorequires an empty record. If refused, the stroke stays open._serializefollowsclay_dynamic_delta_serialize(clay_result+size_t* count, size query,BUFFER_TOO_SMALL) rather than the issue'ssize_treturn, so that a short buffer is retryable.Tests, and proof they bite
tests/unit/test_c_multires_delta.cpp, 14 cases, 703 assertions, through the C ABI only. They cover:commit_intoencoded_bytes== formula == serialize size; spill round trip; truncated / trailing / newer-version bytes refusedThis is a feature, so the file does not compile on main. Each engine decision was proven by mutation:
apply_to_multires)Verification
cmake --preset cpu-only -DCLAY_BUILD_TESTS=ON, full build clean (no warnings);ctest --preset cpu-only: 10/10 passed.-Wall -Wextra -Wpedantic -Wshadow -Werror -fsyntax-onlyon every touched TU and the new test.python3 tools/release_check.py --skip-slow: version, configure, build, tests (11/11), parity, layering, dialect, licenses, task-symbols, bindings (imported pyclay, 768 capabilities), kernels, abi, openspec all PASS.deviceand the fourhardware/*rows fail. These are release-path gates and fail onorigin/maintoo: the engine changed since7f6cf38ff, andbake_volume.hchanged since59e42ccf9, which this PR does not touch.tools/check_test_shards.py: 3048 cases across 4 shards.tools/check_doc_latency.py: OK.npx @fission-ai/openspec@1.12.0 validate --all --strict: 78/78.tools/check_c_abi.py:write_multires_reportandmultires_apply_stroke(are registered as bounded fills. The recorded stroke zeroes*out_applied(asize_t), and the whole-struct heuristic could not tell that apart from a report write.read_version,decode_half) and the stroke frame resolution was extracted (resolve_stroke_frame), so every new or moved function stays under 15.ABI 0.124.0 -> 0.125.0 (additive: twelve entry points, one descriptor; no shipped signature or layout changes). Version lines moved in
CMakeLists.txt,bindings/c/clay.h,pyproject.toml. No scene or multires format change.Closes #671