Repository navigation
Bound an edit by the smooth combines after it (#650) - #653
Merged
leonardoaraujosantos merged 3 commits intoSep 23, 2026
Merged
Conversation
A node is the running value of its chain wherever it is the nearest thing, so an edit changes that value far from the node's own box. A hard union leaves the beyond-band difference alone; a smooth combine further down reads the running value out to its support and carries it back into the band. node_reach_bound dilated only by the enclosing groups, so the node bound -- and the #639 undo narrowing clamped into it -- stopped short of the sibling's fillet. Two r = 0.3 spheres, the second smooth at k = 0.3, the first moved 0.1: band samples moved by up to 0.044 outside the box plus the band, 25 stale bricks after mark_dirty_nodes. Seed 5128 of the undo-bound oracle is the same mechanism at one ulp (#650). node_reach_bound now dilates, at each level before the group's support, by the later siblings' cull_pad_terms resolved at full support (CullPadTerms::support_total); the envelope the cull uses left 0.012. A node followed only by hard combines, including every node appended last, keeps its bound bit for bit. The walk is over the later siblings, so loops paid it per node: a drag frontier over 1,428 warps of a 10,000-item layer went 0.39 -> 65 ms and undoing a 2,134-warp Move 0.85 -> 94.6 ms. ChainDragMemo holds per-chain suffix maxima filled from the end; LayerExtent owns one per query, the drag frontier threads one through its loop, and an undo replay shares one across its commands, cleared after any command but a deformer or colour edit (0.45 and 1.04 ms). The mirrored-ridge frontier test now resumes the four corner bricks the unmirrored control refuses (16/0 against 12/4): the wider reach prepares their prefix seeds, and the bit-exact parity check still holds, so the comparison is >= rather than ==.
The scene-model Influence bounds requirement now states the reach of the smooth combines after a node, with three scenarios added. The proposal carries the measurements and what building it found.
The MSVC job failed the surface-drag allocation gate at 6.0625 per item against 6.0 (194 allocations over 32 items). ChainDragMemo added 13 allocations to every drag: an unordered_map node, and a suffix vector regrown member by member across the chain. MSVC's unordered_map also allocates when it is still empty, which is why only that job crossed the line; libc++ passed at 5.75. The memo now keeps its chains in a flat list, searched linearly -- a query touches one or two chains, and an undo step that keeps the memo across commands keeps it on one -- and reserves each chain's suffix to its full length on first use. Answers are unchanged: same walk, same terms, same indexing. On libc++ the drag now makes 174 allocations, against 184 before this commit and 171 on main. The drag gate cannot hold this on every compiler, so a new gate counts the memo on its own: a 400-member smooth-blended chain asked for every suffix must cost at most 2 allocations. It fails on the previous memo at 12, on libc++, and requires a nonzero answer so a memo that walked nothing cannot pass it. Full unit suite: 2,939 cases, 17,771,052 assertions, 0 failed.
This was referenced Sep 23, 2026
Merged
SummerTree
pushed a commit
to SummerTree/ClayCore
that referenced
this pull request
Oct 7, 2026
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 (CyberdyneCorp#645), bounds narrow per operator (CyberdyneCorp#637), the undo bound reports a grab rather than its node (CyberdyneCorp#648), the node influence bound widens behind a smooth sibling (CyberdyneCorp#653), the brick build keeps a grab the cull cannot judge (CyberdyneCorp#652), voxel sculpt-layer operations and creation are undo steps (CyberdyneCorp#647, CyberdyneCorp#651), and pyclay's multires Layer brushes gain layer_height (CyberdyneCorp#636, CyberdyneCorp#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.
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.
Why
undo_bound_oracle_probeunderRV_RAWBOUND(seed 5128, rich) found the raw field moving by an ulp outside the undo bound plus the band, and the same on main before and after #648. The issue suspected the group blend's support. That's not the cause. The group in that seed is the first root, so its combine never applies (#515).The cause: the target is the first item of its chain, and the item after it blends smooth (k = 0.297). The first item of a chain has no combine. It is the running value, and so is any item wherever it's the nearest thing. So an edit changes that value far from the node's own box. A hard union leaves that beyond-band difference alone (
min()is exact). A smooth combine further down doesn't:csmin(a, b)readsawherever|a - b| < support, so withbin the band it carries a change inaback into the band out to band + support.node_reach_bounddilated only by the enclosing groups. That made the node bound too tight, and so was #639's head narrowing, which clamps the grab ball into that bound. In the seed, the grab's eased rim changes the node's value by an ulp inside the ball, 0.3 from its surface, and the sibling's blend lifts it into the band.It isn't an ulp in general. On main (8b7a571) I placed two r = 0.3 spheres 0.3 apart, blended the second smooth at k = 0.3, and moved the first by 0.1:
mark_dirty_nodesWhat lands
node_reach_boundalso dilates by the smooth combines after the node. At each level, before the group's own support, it adds the later siblings'cull_pad_termsresolved at full support (CullPadTerms::support_total). A later sibling group contributes only its own combine, because its children start their own chain. This widensclay_layer_node_influence_bound,clay_brick_cache_mark_dirty_nodes, the command path, and the undo bound (through the clay_document_undo_bound reports a grab's whole node, so undoing a Move refills the node and its cost grows with the chain #639 clamp).ChainDragMemoholds per-chain suffix maxima, filled from the end only as far as a query needs.LayerExtentowns one per query, the drag frontier passes one through its loop, and an undo step's replay shares one across its commands. The replay clears it after any command except a deformer or colour edit.clay.hgets a NOTE onclay_layer_node_influence_bound,docs/05a section, and thescene-model"Influence bounds" requirement is modified. The change is archived in this PR (2026-09-23-bound-an-edit-by-the-blends-after-it).What measuring refuted
blend_total(the k-multiple fitted to what a cull may drop against fp16) still moved 110 band samples by up to 0.0123 on the fixture above. At full support, 0.Oracle, 400 trials each,
RV_RAWBOUNDThe three rich stale trials (5111, 5128, 5229) are the brick build disagreeing with the raw field. They are identical on main and belong to #649, which this PR doesn't touch.
RV_FORWARDshows 12 stale trials on main and on this branch alike.Tests
Each new case fails on main and passes here:
test_node_reach_bound.cpp:test_c_undo_bound_grab_support.cpp: a grab ball past the node's band, on a smooth sibling's surface, is undone and redone, then compared against a rebuild. Main leaves 4 stale bricks per direction.test_c_frontier_resume.cpp(changed): the mirrored-ridge case now resumes the 4 corner bricks the unmirrored control refuses (16/0 against 12/4). The wider reach prepares their prefix seeds, and the bit-exact parity check against a fresh oracle still holds every frame. Sosplit == plainbecomes>=/<=, with the reason stated in the test.Verification
ctest --preset cpu-only: 10/10 shards pass.check_test_shards: 2,938 cases.tools/release_check.py --skip-slow: all rows pass, includingbindings(imported pyclay),abi,openspec,layering,dialect,kernelsandparity, exceptdevice. That row is the release-time iPad gate, which reports "engine changed since the gate ran at 704f2d4", as it does for main.openspec validate --all --strict@1.12.0: 62 items pass.check_doc_latency: OK.Not covered: a gated node after the edited one lerps the running value, and a lerp of two beyond-band values need not stay beyond band. The intersect surface delta already refuses that case (
chain_carries_only_supports); this term doesn't model it. I left the ROADMAP archive count alone so it won't conflict with the three PRs landing alongside this one.Closes #650