Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions bindings/c/clay.h
Original file line number Diff line number Diff line change
Expand Up @@ -10285,17 +10285,45 @@ clay_result clay_multires_sculptor_apply_stroke(clay_multires_sculptor* sculptor
int32_t defer_normals, size_t* out_applied,
clay_multires_stamp_report* out_report);

/* clay_multires_sculptor_apply_stroke, fed by a stroke session (ABI
/* clay_multires_sculptor_apply_stroke_recorded, fed by a stroke session (ABI
* 0.126.0). The gesture begins the level record MeshBrush::Layer measures
* against once, at its first stamp, and carries a grab's region and a
* snakehook's anchor to the close, as the whole-path call does within one
* call. `out_report` describes this call. */
* call. `out_report` describes this call.
*
* `record` is a clay_multires_delta. Its typedef is just below; the record's
* own calls are documented further down, under clay_multires_delta. It may be
* NULL for an unrecorded gesture, which stamps exactly what a recorded one
* does. Recorded or not, and into which record, is part of the binding: a
* later call naming another record, or NULL after a record, is
* CLAY_ERROR_INVALID_ARGUMENT.
* Every call of the gesture continues the same record, as consecutive
* clay_multires_sculptor_stamp_recorded calls do, so one record across the
* gesture is ONE undo step holding both halves -- the base detail and, with an
* active sculpt pass, that pass -- and equals the record
* clay_multires_sculptor_apply_stroke_recorded makes for the whole path.
*
* The refusals come BEFORE the session's stamps are taken, so a refused call
* loses none of them and the next accepted call applies them: a malformed
* report is CLAY_ERROR_INVALID_ARGUMENT, and a record the hierarchy no longer
* accepts -- another hierarchy, a changed level structure, or a DIFFERENT
* sculpt pass made active mid-gesture while the record holds one -- is
* CLAY_ERROR_SNAPSHOT_MISMATCH, with `*out_applied` 0 and nothing stamped. A
* pass made active mid-gesture while the record holds only base entries is
* ACCEPTED: a base half and a pass half are separate storage, so the record
* then holds both and one revert still takes the whole gesture back.
*
* A call with nothing to apply -- before anything settles, or after the
* close -- leaves the record exactly as it was, binding included. */
typedef struct clay_multires_delta clay_multires_delta;
clay_result clay_multires_sculptor_apply_stroke_tx(clay_multires_sculptor* sculptor,
clay_stroke_tx* tx,
const clay_mesh_brush_desc* brush,
const clay_mask* mask,
const clay_mesh_frame* mesh_to_world,
int32_t defer_normals, size_t* out_applied,
int32_t defer_normals,
clay_multires_delta* record,
size_t* out_applied,
clay_multires_stamp_report* out_report);

/* -- changed-block transport -------------------------------------------------
Expand Down Expand Up @@ -10806,8 +10834,9 @@ clay_result clay_multires_sculpt_layer_stroke_cancel(clay_multires_sculpt_layer_
* same spot record the same entry count and the same encoded bytes.
*
* THREE WAYS IN, ONE RECORD:
* - clay_multires_sculptor_stamp_recorded and _apply_stroke_recorded: the
* plain sculptor. It writes the stack's ACTIVE sculpt pass when there is
* - clay_multires_sculptor_stamp_recorded, _apply_stroke_recorded and the
* stroke-session consumer _apply_stroke_tx (ABI 0.126.0, the record
* continued across every call of the gesture): the plain sculptor. It writes the stack's ACTIVE sculpt pass when there is
* one and the base when there is not, and the record takes whichever half
* was written -- a host does not have to know in advance;
* - clay_multires_sculpt_layer_stroke_commit_into: the transaction's record,
Expand Down Expand Up @@ -10867,7 +10896,8 @@ clay_result clay_multires_sculpt_layer_stroke_cancel(clay_multires_sculpt_layer_
* and a capture into a record is a call on that record too. A replay only
* READS its record. */

typedef struct clay_multires_delta clay_multires_delta;
/* typedef struct clay_multires_delta clay_multires_delta; -- declared above
* clay_multires_sculptor_apply_stroke_tx, the first call that takes one. */

/* An empty, unbound record. Destroy with clay_multires_delta_destroy; NULL is
* a no-op. */
Expand Down
28 changes: 24 additions & 4 deletions bindings/c/clay_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23361,31 +23361,51 @@ clay_result clay_multires_sculptor_apply_stroke_tx(clay_multires_sculptor* sculp
const clay_mesh_brush_desc* brush,
const clay_mask* mask,
const clay_mesh_frame* mesh_to_world,
int32_t defer_normals, size_t* out_applied,
int32_t defer_normals,
clay_multires_delta* record,
size_t* out_applied,
clay_multires_stamp_report* out_report) {
if (out_applied) *out_applied = 0;
if (!sculptor || !sculptor->sculptor)
return fail(CLAY_ERROR_INVALID_ARGUMENT, "null multires sculptor");
clay_result r = probe_multires_report(out_report);
if (r != CLAY_OK) return r;
mesh::MultiresSurface* sp = sculptor->owner ? sculptor->owner->target() : nullptr;
if (!sp) return fail(CLAY_ERROR_NOT_FOUND, "hierarchy is no longer in its document");
SculptBind b;
clay_result r = read_sculpt_bind(*sculptor, brush, mask, mesh_to_world, defer_normals, &b);
r = read_sculpt_bind(*sculptor, brush, mask, mesh_to_world, defer_normals, &b);
if (r != CLAY_OK) return r;

MultiresStrokeSink* sink = nullptr;
r = bind_stroke_sink<MultiresStrokeSink>(
tx, StrokeSinkKind::Multires, sculptor, {bind_key(mask), bind_key(defer_normals)},
tx, StrokeSinkKind::Multires, sculptor,
{bind_key(mask), bind_key(defer_normals), bind_key(record)},
[&](std::unique_ptr<MultiresStrokeSink>* made) {
*made = std::make_unique<MultiresStrokeSink>(*sculptor, tx->tx.preset(),
*sculptor->sculptor, b);
return CLAY_OK;
},
same_descriptors(b), &sink);
if (r != CLAY_OK) return r;
// THE RECORD BEFORE THE STAMPS ARE TAKEN, as on the adaptive consumer: a
// refused call loses none of them, and the next accepted call applies them.
if (!sink->closed) {
r = accept_multires_record(record, *sp);
if (r != CLAY_OK) return r;
}

const std::uint32_t level = sp->sculpt_level();
StrokeBatch batch = next_batch(*sink, sink->source(tx->tx));
const std::size_t applied = sink->gesture.apply(batch.stamps, nullptr);
const std::size_t applied =
sink->gesture.apply(batch.stamps, base_half(record), layer_half(record));
// ONLY A CALL THAT TOOK STAMPS BINDS, and it took them after the check
// above accepted the record. A call with nothing to apply -- above all
// one after the close, which skips the check -- must leave the binding
// alone: re-binding there would stamp a record from before a level change
// with the hierarchy's current structure and let it replay onto a
// hierarchy it does not describe. An empty record needs no bind, and a
// non-empty one was bound by the call that captured into it.
if (record && !batch.stamps.empty()) record->gesture.bind(*sp);
if (out_applied) *out_applied = applied;
if (batch.closes) {
sink->closed = true;
Expand Down
2 changes: 1 addition & 1 deletion docs/05-claycore-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -1902,7 +1902,7 @@ Runnable: [`examples/60_surviving_a_crash.py`](../examples/60_surviving_a_crash.
- **A rebuild through the document** (`clay_document_voxel_remesh_layer`, `Document.voxel_remesh_layer`): the same operation landing on a LAYER, as one undo step, transactional — a refusal, a validation failure or a cancel leaves the layer byte-identical and adds no step. Its undo record is `session::Step::Kind::MeshReplace`, holding the mesh on each side, because a sparse `VertexDeltas` records no indices by design and cannot express a change that replaces them. A per-layer GEOMETRY REVISION moves for a wholesale replacement and never for a sculpt: that asymmetry is what lets a cached adjacency, BVH or sculptor survive a brush stroke and be invalidated by a rebuild, and it is what makes a late-arriving commit refusable rather than silently winning. It also closes a trap neither of the older checks could see — a replacement landing on the same vertex and index counts passes both the mesh-pointer comparison (a `std::map` node's address is stable) and the sculptor's own count check.
- **Welding** (`mesh/weld.h`): merging coincident vertices and removing the triangles that collapses. **The default mesher emits ZERO-AREA TRIANGLES** — 1458 of 70,140 on a plain analytic sphere, two per cent, with two corners at bit-identical positions — and nothing had noticed, for two reasons worth stating: everything downstream tolerates them (an exporter writes them, a BVH holds them, the decimator drops them itself), and `validate` counts them as SLIVERS rather than degenerates, because its `degenerate_triangles` means *repeated indices* and these have distinct indices at identical positions. So a marched mesh reports `clean()` while carrying them. `mesh::DynamicSurface` cannot hold one — a half-edge face with two of the same vertex does not exist — so it refuses, which meant **no mesh this library marched could become an adaptive surface at all** until this shipped. Not the same verb as `Adjacency`'s weld: that groups vertices into classes and leaves the triangle list byte-identical (a seam's duplicates move together under a brush); this merges them and rewrites the triangles. Watertightness survives by construction — a triangle whose corners coincide bounds nothing, so removing it cannot open a hole — a UV seam is preserved by default (duplicated positions with different UVs is how a flat mesh *represents* a seam), quads are dropped on rewrite, every index is in range afterwards, and a mesh with nothing to merge comes back byte-identical.
- **Adaptive-surface undo** (`clay_dynamic_delta`, pyclay `TopologyDelta`, ABI 0.118.0): a host-owned record of one adaptive gesture (`mesh::RecordedGesture`, a `TopologyDelta` plus the surface marks at both ends), captured by `clay_dynamic_sculptor_stamp_recorded` and replayed by `clay_dynamic_delta_revert` / `_apply` **through the sculptor**. The host owns the sequence: how deep the stack is, when a record is dropped, how an undo interleaves with its own document edits. The engine owns one thing, the proof that a record matches the surface before it writes. **Records replay last in, first out, and nothing else.** Every surface carries a `{lineage, epoch}` mark, and a replay onto any state other than the record's end state is `CLAY_ERROR_SNAPSHOT_MISMATCH` with nothing written. This is needed even for strokes that never touch, because a later stroke reuses the slots an earlier one freed. Replaying onto the target state is `CLAY_OK` and writes nothing. The sculptor's chunk index and dirty-chunk stream follow the replay incrementally (0.1 ms at 49k faces against 39 ms for `rebuild_index`), so no rebuild is needed; one clears the dirty set and renumbers chunks. A replay restores `to_mesh` and the stored normals byte for byte over live elements, but not `clay_dynamic_surface_serialize` bytes, since dead slots stay. **Budgeting**: `clay_dynamic_delta_stats.resident_bytes` is what a record holds, allocator-dependent, and the memory ledger does not count it. `encoded_bytes` is exact (`56 + 122V + 114H + 42E + 66F`). **Spilling**: `clay_dynamic_delta_serialize` writes a record out and `_deserialize` reads it back to replay onto the same surface handle. It is not crash recovery, because a reloaded surface is a new lineage. One more limit: a replay keeps only the sculptor it runs through in step, so a second sculptor over the same surface is left stale, just as it is by a stamp. See `docs/07-brushes-and-features.md` § 8b.
- **Multiresolution undo** (`clay_multires_delta`, ABI 0.125.0, issue #671): a host-owned record of one gesture on a hierarchy (`mesh::MultiresGesture`: a `MultiresDelta` base half, a `SculptLayerDelta` pass half, and the hierarchy's `structure_revision` plus a per-process origin at capture). Captured by `clay_multires_sculptor_stamp_recorded` / `_apply_stroke_recorded` (the plain sculptor, which writes the active pass when there is one) and by `clay_multires_sculpt_layer_stroke_commit_into`; replayed by `clay_multires_delta_revert` / `_apply` **through the hierarchy**, bit-identically, idempotently, and with the moved patches in `clay_multires_dirty_blocks`. **Bound to its structure**: another hierarchy, a level added or removed (even one later undone), or a pass the stack no longer has is `CLAY_ERROR_SNAPSHOT_MISMATCH` with nothing written; no ordering is enforced beyond that, so overlapping records replay last in, first out by the host. `encoded_bytes` is exact (`40 + 16 + 32d + 28c` for a base half, `+ 24 + 32ld + 16lm` for a pass half); `resident_bytes` is the budget figure and the memory ledger does not count it. Serialize is for spilling within the process. See `docs/07-brushes-and-features.md` § 8d.
- **Multiresolution undo** (`clay_multires_delta`, ABI 0.125.0, issue #671): a host-owned record of one gesture on a hierarchy (`mesh::MultiresGesture`: a `MultiresDelta` base half, a `SculptLayerDelta` pass half, and the hierarchy's `structure_revision` plus a per-process origin at capture). Captured by the plain sculptor's recorded calls, which write the active pass when there is one: `clay_multires_sculptor_stamp_recorded`, `_apply_stroke_recorded`, and the stroke-session consumer `_apply_stroke_tx` (ABI 0.126.0), which continues one record across the gesture's calls and checks it before the session's stamps are taken. Also captured by `clay_multires_sculpt_layer_stroke_commit_into`. Replayed by `clay_multires_delta_revert` / `_apply` **through the hierarchy**, bit-identically, idempotently, and with the moved patches in `clay_multires_dirty_blocks`. **Bound to its structure**: another hierarchy, a level added or removed (even one later undone), or a pass the stack no longer has is `CLAY_ERROR_SNAPSHOT_MISMATCH` with nothing written; no ordering is enforced beyond that, so overlapping records replay last in, first out by the host. `encoded_bytes` is exact (`40 + 16 + 32d + 28c` for a base half, `+ 24 + 32ld + 16lm` for a pass half); `resident_bytes` is the budget figure and the memory ledger does not count it. Serialize is for spilling within the process. See `docs/07-brushes-and-features.md` § 8d.
- **Spatial scalar transfer** (`mesh/transfer.h`): resampling a caller-owned per-vertex array — a mask, a weight — from one mesh onto another by closest point. Mask is not a `Mesh` field and does not become one to suit an operation; this is what carries it across a topology replacement.
- **The dual meshers walk the cell range once.** Surface nets, dual contouring and the quad mesher share `dual_grid_mesh`, which used to place every cell's vertex in one pass and then walk the whole range again to emit quads — re-reading four lattice corners it already had, for every cell including the ones that own no vertex and can contribute no quad. It now emits a cell's quads as it places its vertex, which is sound because a quad only ever references cells reached by stepping BACK along the two axes that are not the edge's: the owning cell and three already placed. The sampler is also templated rather than a `std::function`, so the tape meshers' lambda inlines — eight corners a cell is 33M calls on a 0.02 lattice, and none of them was inlining. Together 113 ms → 75 ms serial, output byte-identical across all five entry points (#304).
- **Brick meshing runs the march across bricks in parallel and welds serially.** Marching a brick is pure — it reads the cache, which is a const lookup — so bricks fan out over `clay/parallel`. The WELDING cannot: one `Builder` serves every brick precisely so a lattice edge shared by two of them yields one vertex, which is what keeps the sparse set watertight at brick seams. So the march records what each brick would emit and a serial pass replays those recordings through the single builder in key order, calling `edge_vertex` in the same sequence the serial loop did — the result is byte-identical to the serial path by construction, not by tolerance. Measured on a 24-core desktop: 276 bricks 30.9 ms → 7.5 ms.
Expand Down
15 changes: 13 additions & 2 deletions docs/07-brushes-and-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,15 @@ rebuilt descriptor with the same values matches), is refused. The call after
gesture is open on its target: a grab's carried region, the deferred normals,
and — for SDF, voxel and mask targets — the owning document's undo group,
which is what makes a gesture one undo step.
The sculptor consumers take the record their whole-path recorded call takes: a
`clay_mesh_deltas` for a mesh, a `clay_dynamic_delta` for an adaptive surface,
and a `clay_multires_delta` for a hierarchy. Passing one record to every call of
the gesture (NULL for none; which one is part of the binding) gives the record
the whole-path call gives, both of a hierarchy's halves included, so one
`clay_multires_delta_revert` takes the gesture back on every level. The adaptive
and multires records are checked before the call takes its stamps: a refusal
(`CLAY_ERROR_SNAPSHOT_MISMATCH`, e.g. another sculpt pass made active
mid-gesture) loses none of them.
The mesh consumers are the same code as their whole-path calls
(`MeshStrokeGesture`, `MultiresStrokeGesture`, `DynamicStrokeGesture`): a stroke
per call would re-gather a grab's region at every batch and land elsewhere.
Expand Down Expand Up @@ -2464,8 +2473,10 @@ write: the base half (`MultiresDelta`: cage positions at level 0, a level's own
coefficients above it) and the layer half (`SculptLayerDelta`: one pass's
coefficients and mask weights).

Three calls capture into it. `clay_multires_sculptor_stamp_recorded` and
`_apply_stroke_recorded` are the plain sculptor's calls with a record; the plain
Four calls capture into it. `clay_multires_sculptor_stamp_recorded`,
`_apply_stroke_recorded` and the stroke-session consumer `_apply_stroke_tx` (§ 5,
one record continued across every call of the gesture) are the plain sculptor's
calls with a record; the plain
sculptor writes the **active pass** when there is one, so the record takes
whichever half was written and a host does not have to know in advance which.
`clay_multires_sculpt_layer_stroke_commit_into` closes a layered transaction and
Expand Down
Loading
Loading