The brush commits via a GPU-canvas readback and relies on the action's first
execute() to SET raw_pixels from that readback — at that point raw_pixels is empty
(new keyframe) or the pre-stroke state, never something a dirty-rect diff can stamp
onto. My initial diff-only execute skipped (to avoid corruption), so the stroke
disappeared.
Fix: the action keeps the full post-edit buffer ONLY for the first execute (the
commit), assigning raw_pixels outright exactly like the old code; it's taken/dropped
immediately, so the action sitting in the undo stack still retains just the small
diff. Redo replays via the diff onto the now-resident base.
Also harden the diff itself for the blank-base case: `before_blank` lets apply_after
build from a transparent buffer (redo of a first stroke after undo-to-blank) and
apply_before restore to empty; the resident-base skip is kept only for non-blank
bases (faulted in before undo/redo). Tests cover commit/redo from empty.
`RasterStrokeAction`/`RasterFillAction` stored the whole before+after RGBA frame
(~16 MB/action at 1080p → up to ~1.6 GB at the 100-action cap). They now store a
`RasterDiff` — only the changed bounding box's pixels before and after — computed
once in `new()` from the full buffers, which are then dropped. A brush dab shrinks
from ~16 MB to tens of KB; a full-canvas fill is unchanged (its bbox is the frame).
Paging interaction: a diff overwrites just the bbox, so the keyframe's pixels must
be resident when undo/redo applies. A clean evicted frame's container bytes equal
its current logical state, so the editor faults the target frame in (synchronously)
before undo/redo via a new `Action::raster_resident_hint` + `peek_undo/redo_raster_hint`.
Dirty frames are never evicted, so they're already resident. If a base is somehow
not resident the apply is skipped (logged), never resized-and-corrupted.
Unit tests cover exact before/after round-trip, blank-first-stroke, no-op, and the
non-resident-base skip.