Repository navigation
Carry a seed across a region edit only if no append since reached it - #668
Merged
Merged
Conversation
A region invalidation advanced every seed its box missed to the new revision, including seeds still sitting at the revision before an append that had not been refilled yet. An append re-stamps no seed (the append log carries it), and the region edit forgets that log, so those seeds became "current" while holding the pre-append field, and the refill's rev == now shortcut returned it on every later refill. Appending an intersect and moving it before a refill hit this: the append changes the whole layer, the #471 swept move box does not cover it, and the cache kept the un-intersected sphere (1171 surface bricks against 192) even after the whole bound was re-dirtied. A local append followed by an edit elsewhere does the same (1123 against 1116). The log now records each append's command_influence_bound. A lagging seed is advanced only when the log covers every revision it lags by and no append in that span reaches its cull region, with the log's union as an O(1) early out. All three region fronts read the log before forgetting it. Seeds no append reached are still carried and resumed. Fixes #665
The #665 carry check scanned the append log from each lagging seed's revision, so the first region edit after a stroke cost seeds x appends under cache_mutex_. Measured on a filled unit sphere (dim 8, voxel 0.02) with N cap dabs each refilled on its own, the first far move took 3.98 / 14.5 / 42.9 ms at 200 / 1000 / 3000 dabs, against 0.21-0.30 ms on main, and kept growing with the stroke. The region edit now builds a binary tree of unions over the log's index ranges once (ReachTree, O(n)) and each seed asks whether any reach from its own index on meets its cull region: 0.52 / 0.89 / 1.27 ms. Interior unions only prune and leaves decide, so the answer is exactly the scan's. Infinite reaches are widened to the whole of space when logged so no union can prune them; the separate running union is gone. Two cases pin the log offset across several appends: a later append that reaches a brick holds its seed back, and an append the brick was refilled after does not. Reading the log from its start, skipping one entry, or testing only the first entry each fail at least one case.
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.
Fixes #665
Why
If you append an
Op::Intersectitem and then move it with a uniform transform before any refill, the brick cache keeps the un-intersected layer. Re-dirtying the item's whole bound and refilling does not repair it. The document is correct, only the bricks are stale.Cause (confirmed by instrumenting, not just by reading the code)
The issue suspected the frontier path at the appended ordinal. The actual cause is in
touch_region_locked, which all three region fronts share. An append re-stamps no seed, because the append log is what carries a seed across appends. The next region edit then forgot the log and moved every seed its box missed to the new revision. That included seeds still sitting at the revision before the append. A probe showed the stale bricks going from rev 3 to rev 5 with the append at rev 4. From then on therev == nowshortcut returned the pre-append field on every refill. The same happens with no intersect at all: append a local item, then edit something far away.What lands
touch_appendedrecords each append'scommand_influence_bound, whichapply_editalready computes, next to the log. An infinite bound is widened to the whole of space when it is logged.New
carried_across_appends: a region edit moves an unreached, clean seed forward only if one of two things holds:ReachTree, O(n)), so each seed costs about log(n) box tests. Interior unions only prune and leaves decide, so the answer is exactly a scan's.Any other seed stays at its old revision, and its next refill walks it in full.
All three region fronts read the log before they forget it.
No change to the ABI, the file format or the version.
Measured (issue fixture: sphere r1, cylinder 0.25/1.6 moved to y 0.9, dim 8, voxel 0.02, band 3)
Cost of the check (first far move after N cap dabs, each refilled on its own; filled unit sphere, dim 8, voxel 0.02)
The scan was seeds x appends under
cache_mutex_and grew with the stroke; review replaced it with the tree.Tests
New file
tests/unit/test_c_intersect_append_move.cppwith 11 cases. It covers the issue's table (stale variants plus the correct controls) and adds one case showing that a seed no append reached is still resumed, checked throughclay_document_resume_stats. Two more pin the log offset across several appends: a later append that reaches a brick holds its seed back, and an append the brick was refilled after does not.Mutation check:
Verification
ctest --preset cpu-onlywith tests and pyclay on: 11/11 passed.openspec validate --all --strict(1.12.0): 65/65 passed.-O2 -Wall -Wextra -Wpedantic -Wshadow -Werror -fno-rtti(full compile, not-fsyntax-only, so the flow-based warnings run) onclay_c.cppand on the new test: clean.touch_region_locked13,carried_across_appends4,ReachTree::any_in4,touch_appended3.tools/release_check.py --skip-slowon the reviewed head: every row passes exceptdevice, which is red only because the engine changed since the last recorded device gate (7f6cf38); main carries other engine changes that also expire it. That is a release-time gate.check_bench.Docs
docs/05-claycore-library.md: the seed carry-forward paragraph now states the new rule.refill-a-dirtied-brick-against-the-current-document, with an ADDED brick-cache requirement.