Make Liquify redraw the brush, not the layer - #9
Merged
Conversation
Every pointer move re-warped the whole layer: flatten the entire
snapshot to f32, resample all of it, clone the tile map, write every
tile back, and damage the layer rect so the compositor recomposited the
canvas. On a 12-megapixel layer that was a second per mouse move, and
Enter cost another one and a third.
A dab only moves the mesh vertices under the brush, and a destination
pixel interpolates the four vertices around it, so nothing outside the
brush plus a cell can change. Render that rect and no more: `dab_rect`
gives it, `warp_into` resamples into the layer's own tiles rather than
rebuilding them from a clone, and the damage shrinks with it.
The source the kernel gets shrinks too. A token still means "the whole
snapshot, keep it resident" — Puppet Warp's case, and the one an
accelerated backend can use — while no token now also means the plane is
cropped to what the displacement over this region can reach. `subgrid`
does the same for the offsets, which were a megabyte and a half copied
per pointer move.
Commit then has nothing left to sweep: the layer already holds the
finished warp, so it moves the tiles out and puts the snapshot back.
Tiles no dab reached are still shared with it and the edit compares by
pointer, so history carries only what moved.
The rest is the two conversions either side of the kernel, which now
dominate a big dab: `flatten` goes tile-wise and parallel instead of a
map lookup per pixel, `warp_cpu` puts its rows across the cores, and
picking the tool up takes tile-granular bounds rather than scanning
twelve megapixels of alpha for a mesh fringe nobody warps.
Per pointer move, from `examples/liquifybench`:
4000x3000, 100px brush 1045 ms -> 0.8 ms
4000x3000, 1000px brush 1318 ms -> 21.5 ms
1024x768, 100px brush 50.7 ms -> 0.9 ms
with Enter down from 1356 ms to 1.0 ms and picking the tool up from
97 ms to 0.1 ms.
Liquify no longer goes near the GPU warp — a dab is far below what a
round trip pays for — so the comments naming it as the resident-source
client now name Puppet Warp, which still is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Liquify felt laggy because every pointer move re-warped the whole layer: flatten the entire snapshot to f32, resample all of it, clone the tile map, write every tile back, and damage the layer rect so the compositor recomposited the canvas. On a 12-megapixel layer that was ~1 s per mouse move, and Enter cost another 1.3 s.
What changed
radius + one cellcan change.Mesh::dab_rectreturns that rect;warp_intoresamples into the layer's own tiles instead of rebuilding them from a clone; damage shrinks to the same rect, so the compositor invalidates a handful of tiles.src_token != 0still means "the whole snapshot, keep it resident" — Puppet Warp's case, and the one an accelerated backend can use. No token now also means the plane is cropped to what the displacement over the region can reach.Mesh::subgriddoes the same for the offsets, which were a megabyte and a half copied per pointer move.Arc-shared with the snapshot andreplace_layer_tilesdiffs by pointer, so undo history carries only what moved.flattengoes tile-wise and parallel instead of a map lookup per pixel,warp_cpuputs its rows across the cores (both also help Puppet Warp and any GPU-declined fallback), and picking the tool up takes tile-granular bounds rather than scanning twelve megapixels of alpha.Numbers
cargo run --release -p schist-tools-warp --example liquifybench(new):Testing
tests/incremental_warp.rsholds the invariant the speedup rests on: a stroke rendered dab by dab is pixel-identical to one sweep of the final mesh. Plus a dab touching nothing outside its footprint, a cropped plane reaching as far as the displacement does, and tool-level tests for damage size, commit/undo and Escape. Workspace tests and clippy are clean, and the GPU warp-parity tests still pass.Also driven end to end through the headless MCP server on a real image — smooth warp, no seams or stale patches, undo restored the file byte-identically.
Notes
🤖 Generated with Claude Code