Three Hours Old
Ariandel's repository was born on the evening of 2026-10-01. Before that night was over, it had an engine with layers, history and a file format, an app with its first TestFlight builds, and a green test suite. Then Dad asked for two things, in this order: "1. comprehensive code review 2. add missing essential features."
The review read the whole repository: engine, C bridge, renderer, shaders, app and tests. Its first finding, in the words of the coop history row that records it (#5658):
"Autosave copied every painted tile on the main thread (ArtworkArchive copied Data per tile): hundreds of MB per save on a large painting."
The Code Was Honest, and Still Wrong
Look at the old version in the first code block. Nothing was hidden. Its docstring said exactly what it did: "Taking an archive copies tile bytes on the artwork's thread; encoding, compressing and writing can then run anywhere." The design was half right. Encoding already ran off the main thread, in a detached task. And the copy was not carelessness, it was the price of safety: without some way to promise that a buffer will not change, handing a live tile to a background thread means the next dab can rewrite the pixels while the compressor reads them. Copying was the only promise the engine knew how to make.
The cost hid in the word copies. Each tile's .data was a fresh 32 KiB Data, for every painted tile of every layer, on the thread that also receives Apple Pencil input. And autosave was a debounce: four seconds after the artwork last reported a change, a save fired. So every pause in painting long enough to lift the pen and look triggered a full copy of the painting. Run the first lesson's arithmetic: a fully painted 4096×4096 layer is 128 MiB, so a few such layers is hundreds of megabytes, exactly the review's figure.
Why the Tests Were Green
The engine suite passed before the review and after it. That is not a flaw in the suite; it is what pass-or-fail tests measure. A test saves a small canvas, reads it back, and compares pixels: correct. A copy of a few dozen tiles takes no time worth noticing. The defect was never a wrong answer. It was a cost that grows with the size of the painting, on the one thread where time is the product. The question that finds it is the one a reader asks of each line: what does this cost when the painting is big, and which thread pays?
The Fix: Freeze, Don't Copy
Commit 6479f1f landed the copy-on-write tiles from the previous lesson, and the archive became their first beneficiary. LayerArchive.tiles changed from [(key, bytes: Data)] to [(key, tile: FrozenTile)], and the archive line changed from copying each tile's data to calling frozenTiles(). In the commit's own words: "A stroke now costs one tile copy instead of two, an undo step's after-state shares the live tile, duplicating a layer copies nothing, and ArtworkArchive freezes instead of copying, so autosave no longer copies every painted tile on the main thread and saves and exports can encode off it."
Today's save (second code block) freezes on the main thread, which costs little more than setting a flag per tile, then compresses and writes on an actor, while painting continues. The next stroke pays one copy for each tile it touches (one per batch if it is held to a selection or laid through a grain), and only those. Be precise about what is still on the main thread: the two flatten calls for the gallery preview (2048 px) and thumbnail (512 px). Each one uploads any dirty tiles into the GPU mirror, composites the whole canvas at full size, scales it down, and makes the main thread wait for the GPU to finish before it reads the small result back. So their cost grows with the canvas, not with the preview size. What they no longer do is copy every painted tile's bytes, and that copy was the review's finding.
The Same Review's Other Finds
The autosave was the headline, not the whole list. The same review fixed, among smaller things:
- Merge down went through the 8-bit display mirror, erasing the color of faint paint in the lower layer (the faint orange from lesson one). It now uploads both layers at 16 bits and composites in 32-bit float.
- The memory-pressure handling the architecture named did not exist.
History.shednow answers the system's memory warning, oldest undo steps first, layers never. - A background save could be suspended mid-write. Saves now ask iOS for background time.
- Canvas size allowed 16384×16384 regardless of device memory, where the GPU mirror alone would exceed RAM.
CanvasSize.maximumPixelsnow follows the device. - The display had no mipmaps, so the canvas shimmered at zoom-out.
Track 6 takes most of these apart. Notice the shape of the second one: a document described a safety mechanism and the code did not have it. No test fails for a function nobody wrote.
The verification on the record: on the simulator, the engine suite ran 57 green (25 new), the hosted app tests 20, the UI tests 2. The record also names what was not exercised: Pencil pressure, tilt and hover feel, QuickShape timing with a real Pencil, and voice. A review is not a device test, and it said so.