The Unit libmypaint Thinks In
libmypaint never sees a canvas. It sees tiles. When a dab lands, the engine works out which tiles the dab's box overlaps and asks its host for each one, by tile coordinate, through a request that says whether it only wants to read. The host lends a buffer; libmypaint blends into it; the host gets it back. Everything else about Ariandel's pixels follows from the shape of that buffer, so it is worth knowing it cold.
A tile is 64×64 pixels (MYPAINT_TILE_SIZE, set to 64 in libmypaint's mypaint-config.h), four channels per pixel, RGBA, premultiplied, and every channel is a uint16_t in 15-bit fixed point: 0 is nothing, and 1.0 is 1 << 15, which is 32,768. Ariandel's C bridge names the three numbers once, in ariandel_surface.h: ARD_TILE_SIZE 64, ARD_TILE_VALUES (64 × 64 × 4), and ARD_FIX15_ONE (1 << 15). The Swift side mirrors them in a Tile enum whose size is read from the C macro, so there is exactly one place the size is decided.
Do the arithmetic once and keep it: 16,384 values per tile, two bytes each, 32,768 bytes, 32 KiB, per tile. A 4096×4096 layer is 64 × 64 = 4,096 tiles, so if every tile is painted, that one layer is 128 MiB of native pixels. Hold that number. It is the whole of the third lesson.
Sparse by Construction
Each layer's pixels live in a TileStore, a dictionary from TileKey to buffer. An absent key means a transparent tile, so an empty layer costs nothing and a sketch in one corner of a huge canvas costs only the corner. The store keeps itself sparse: setTile drops a buffer that is fully transparent instead of storing it, and at the end of every stroke the painter calls prune on the tiles it touched, because libmypaint asks for every tile a dab's box overlaps and an eraser can leave some of them empty.
One small piece of care is worth copying. TileKey(containingPixelX:y:) floors negative coordinates instead of truncating them toward zero, so pixel −1 lands in tile −1, not tile 0. Symmetry and clone offsets can put a dab left of the origin, and a truncating division would quietly paint it into the wrong tile.
Why Fifteen Bits and Not Sixteen
Read libmypaint's ordinary blend (the second code block) and the choice explains itself. For each pixel, the dab's strength becomes opa_a, the remainder becomes opa_b = (1<<15) - opa_a, and every channel is (opa_a*top + opa_b*bottom) / (1<<15). Because 1.0 is a power of two, that divide is a shift. Because each factor is at most 215 and the two weights sum to 215, the whole numerator stays at or below 230, comfortably inside a 32-bit unsigned integer. And 32,768 itself fits in a uint16_t, so fully opaque is exactly representable. Make 1.0 the next power of two, 65,536, and it no longer fits in a uint16_t; make it 65,535 instead and you lose the shift and the headroom together: the divide becomes a real division, and the numerator climbs to within a hair of 232. Those are properties of the arithmetic you can read off the page, not a quote from libmypaint's authors.
Integers also do not drift in storage. A value written is the value read back, and Ariandel's package stores these tiles losslessly, in this very format, which is why its file format can promise that a reopened artwork is bit-identical.
Why Premultiplied
Premultiplied means each color channel is already multiplied by alpha. That is what makes the blend above a single multiply-add per channel: result = top + (1 − top alpha) × bottom, alpha included, with no division. Straight alpha needs a divide by the resulting alpha to get back to a color. Premultiplied also gives transparency one representation: a pixel with zero alpha has zero color, which is why Ariandel's ard_fix15_is_transparent only has to look at the alpha channel to decide a tile can be dropped.
Leaving Fix15: Two Exits, One Rounding
Nothing outside the engine speaks fix15, so pixels leave through small C loops in ariandel_pixels.c. Two matter here. ard_fix15_to_rgba8_premultiplied feeds the GPU's display mirror: each value becomes (v * 255 + 16384) >> 15, which is round-to-nearest, clamped. ard_fix15_to_rgba8_straight feeds PSD layer channels and layer thumbnails: it first un-premultiplies in fix15, ((c << 15) + a / 2) / a, clamped to one, and only then rescales to 8 bits with the same rounding. A pixel with zero alpha becomes all zeros.
The order matters, and the sketch below shows why. A warm orange at 1% alpha is 262, 66, 33, 328 in fix15. Through the straight exit it comes out as 204, 51, 26: the orange survives. Through the premultiplied 8-bit exit it becomes 2, 1, 0 at alpha 3, and if anyone later divides that back out, the color reads 170, 85, 0. Eight bits of premultiplied color simply do not have room for a faint pixel's hue. That is fine for a picture on a screen. It is fatal for anything that would write pixels back into a layer, and Ariandel learned that the hard way (lesson three, and Track 6's merge story).