An Engine That Paints Anywhere
libmypaint has no idea what a selection is. Its surface interface is a handful of calls: draw a dab here, tell me the color around there, begin a batch, end a batch. There is no mask parameter anywhere in it, and there never will be, because Ariandel vendors libmypaint byte for byte and never patches it (invariant 1). Yet Dad lassos a region, paints across its edge, and the paint stops at the line. Something has to make that true without the engine's cooperation.
The house sign states the rule for this whole track in one sentence: libmypaint never learns about selections, Clone, dual brushes, shapes, grain, stabilization or taper. Each of those is done in one of three places: before the mapping (the pen's input is moved, or the brush's radius and pressure scaled, before the engine sees them), inside the stamp (Ariandel's own C surface lays a dab where libmypaint asks for one: shaped tips, Photoshop's dual brush, a stamped grain), or after the batch (the engine's pixels are merged with something afterwards). Selections are the purest example of the third kind, so they go first.
The Batch Is the Smallest Thing the Host Sees
Painter.continueStroke feeds one frame's worth of coalesced touches at a time. It opens the surface with surface.begin(), hands each input to brush.stroke(to:on:), and closes it with surface.end(). That bracket is the batch.
Why not clip each dab as it lands? Because, for a round dab, there is no "as it lands" that Swift is ever told about. libmypaint's tiled surface queues round dabs per tile and renders a tile's queue later: inside mypaint_surface_end_atomic, or earlier when get_color flushes the tile to sample it. Swift does get called during a batch, but only as the tile store: libmypaint asks for a tile's buffer and hands it back, and Ariandel's own shaped stamp acquires the tiles it lays into. None of those calls says which dab is writing. To clip inside the dab you would have to patch libmypaint's dab renderer, which the vendoring rule forbids. Or you would have to teach every path that writes pixels about the mask: the round dab, Ariandel's own shaped stamp, each symmetry reflection. Right after surface.end() returns, every path's paint for the batch has landed and Ariandel holds all of it at once, so that is where the rule lives. The Painter's own doc comment says it plainly: "libmypaint never learns about selections; the rule lives here, once."
Recording What the Batch Overwrote
To undo a batch's paint outside the selection, you need what those tiles looked like before the batch. So when a stroke has a selection (or a grain, more on that below), continueStroke calls beginBatchRecording() on the layer's TileStore. From then on, the first write to any tile in this batch passes through noteWrite, which freezes the tile's current buffer and keeps it as that tile's before-state. The write then lands in a copy, because a frozen tile is never written. After surface.end(), endBatchRecording() hands back a TileSnapshot: every tile the batch touched, each with its state before the batch (or nil if the tile did not exist).
Three Kinds of Tile
keepInside (first code block) walks that snapshot and asks the selection one question per tile: coverageClass(of:).
.all: every pixel of the tile is fully selected. Keep the paint; there is nothing to do..none: no pixel is selected. Put the before tile back wholesale withsetTile. No arithmetic, and the store simply shares the frozen buffer..partial: the selection's edge crosses this tile. Extract the tile's 64×64 slice of the 8-bit selection mask and runard_fix15_masked_merge(second code block).
The kernel computes, per channel, before + (after − before) · m / 255, with rounding toward the nearest value. At m = 255 it skips the pixel; at m = 0 it copies the before pixel back. The channels are premultiplied 15-bit fixed point (Track 3), and a straight weighted blend of two premultiplied pixels is itself a valid premultiplied pixel. So the merge needs no un-premultiply, no division by alpha, nothing clever. That is a quiet benefit of libmypaint's format choice.
What It Costs
Copy-on-write makes an ordinary stroke cheap: the stroke's undo recording freezes a tile at its first write, the write lands in a copy, and every later batch writes that copy in place. One copy per touched tile per stroke.
The batch recording breaks that. It freezes again at the start of every batch, so the next batch's first write to the same tile lands in a fresh copy. With a selection, the cost is one copy per touched tile per batch. A tile is 64 × 64 pixels × 4 channels × 2 bytes, 32 KiB. A stroke that spends 100 batches scrubbing over the same 20 tiles copies 2,000 tiles, 62.5 MiB of memory traffic over its life, where the same stroke without a selection copies 20. The third code block is a compiled model of exactly this bookkeeping: four tiles, ten batches, 4 copies without a selection and 40 with one.
That is the trade Ariandel chose with its eyes open: a cost that scales with batches, paid only while a selection exists, in exchange for an engine that stays untouched and a rule that lives in one function.
Two Honest Edges
First, the merge works per batch, and the before-state is per batch. A half-selected pixel keeps half of each batch's change, and the next batch starts from that result. Scrub long enough over a feathered edge and the paint keeps building there: the model's half-selected tile climbs from 1000 to 3510 over ten batches while the fully selected one reaches 6000. A feather limits how fast paint arrives, not how much can arrive.
Second, inside a single batch, libmypaint can read back its own not-yet-clipped paint. A smudging brush samples the canvas with get_color, which first flushes the dabs queued on those tiles. So within one batch, a sample taken near the edge can include paint that the merge is about to remove. The code admits this by construction; it is the price of clipping per batch rather than per dab.