The Invariant, Read Against the Code
Ariandel's architecture states its second invariant in one line: "CPU tiles are ground truth; GPU textures are a cache." Every layer is a sparse map of fix15 tiles in memory the CPU owns; Metal textures mirror the tiles that changed, for display and compositing. The same paragraph goes on to say that saving, exporting and showing Pippa the canvas "read tiles, never the GPU." Hold that second sentence up to the code and it is too strong. The project's house rules phrase it precisely, and that phrasing is the one to learn: "whatever reads back from Metal composites tiles uploaded for that read." Tiles are the source. The GPU is an instrument, synchronized first.
What the GPU Holds, and Why It Cannot Be the Truth
The renderer keeps every layer, and every mask's paint, as one slice of a single texture2DArray in .rgba8Unorm, premultiplied, with .private storage. The composite it draws to the screen is rgba16Float and mipmapped. Two facts already rule the mirror out as a source. It is 8-bit premultiplied, so it cannot carry the faint colors fix15 carries (the 1% orange from the first lesson comes back from it as a different color). And private storage means the CPU cannot read it at all without a GPU pass that copies it somewhere shared. The mirror is built to be looked at, not to be believed.
How the Cache Stays Honest
encodeSynchronization (first code block) runs before every frame and at the start of every flatten:
- Slots follow the stack. A layer or mask that left gives its slice back. A new one gets a cleared slice, and its store is told
markAllDirty(), so its first upload is complete. - Only dirty tiles travel. For each mirrored layer it drains
collectDirty(), the set thatwritableTile,setTileand libmypaint's writable releases fill (lesson two). Each dirty tile is converted withard_fix15_to_rgba8_premultipliedinto a shared staging buffer and blitted into its slice; a tile that is now absent uploads as zeros. - Only the changed region recomposites, unless the stack itself changed (order, visibility, opacity, blend, clipping, background), in which case the whole canvas does.
Pixels flow one way: tiles to textures. The engine never writes mirror pixels back into a layer's tiles. Where the app uses the composite internally, it uses it as a reference: ColorDrop and the magic wand read it to decide where to reach, then paint or select with values of their own. (Copy All puts the picture on the clipboard like any other image; pasting it back is an import.)
Who Reads What
| Reader | What it reads |
|---|---|
The .ariandel package | Frozen fix15 tiles, LZFSE-compressed. No GPU. |
| PSD layer channels, per-layer PNG export | Tiles, converted on the CPU. No GPU. |
| Gallery preview and thumbnail | flatten at 2048 and 512 px. |
| Pippa's canvas look | flatten, at most 1568 px on the long edge, as JPEG. |
| PNG, JPEG, TIFF, PDF export, PSD composite | flatten. |
| Merge down | Its own 16-bit arrays and a 32-bit float target. Never the 8-bit mirror. |
flatten (second code block) is the important row. Its docstring says it "synchronizes from tiles first, so it never shows a stale mirror": it runs the same synchronization, composites the whole canvas into a fresh rgba16Float texture, packs that to straight 8-bit and reads it back. So Pippa does see a GPU composite, but a composite of tiles uploaded for that very read. Notice what it composites from: the same 8-bit slices the display uses. For a picture that leaves the app as 8 bits anyway, the loss is small but real: a straight-alpha PNG could have kept faint colors the premultiplied mirror already lost (Track 6, lesson 4 shows one). For anything that writes pixels back into a layer, that loss is not acceptable, which is exactly why merge down builds its own 16-bit arrays. That is the honest version of "never the GPU," and it is the one to keep in your head when you add a reader.
Why the Split Survives Trouble
A memory warning. When iOS warns, the app sheds undo history down to a quarter of its bytes, oldest undo steps first and redo steps only after those, and if an undo step went, a short notice tells Dad. Redo steps go silently: the notice watches only the undo stack. Layers are never touched, because layers are the truth; history is the one thing that can be let go without losing the picture. Track 6 has the details.
A lost cache. Because the mirror holds nothing the tiles cannot regenerate, any renderer can be thrown away and rebuilt. The production code already does this whenever a renderer meets a layer it has never seen: on a canvas resize it drops every texture, and each layer is re-uploaded whole from its tiles. The engine suite proves the property directly (third code block): after strokes, a new layer and an undo, a brand-new CanvasRenderer with an empty mirror flattens to byte-identical pixels. And the save treats the GPU as optional: both preview flattens are try?, so a failed composite costs the gallery a picture, never the painting.
Be exact about what is not claimed. Ariandel has no dedicated handler for a GPU reset, and nobody has forced one on Dad's iPad. What the split guarantees is the shape of recovery: if the textures ever vanish, recovery is a rebuild from tiles, not a loss. That guarantee is designed and tested in the suite; it has not been demonstrated on hardware.