Skip to content
C.W.K.
Stream
Lesson 01 of 04 · published

Two Engines on the Bench

~14 min · evaluation, libmypaint, open-source, measurement

Level 0Blank Canvas
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Size of the Dream

Ariandel started with one sentence from Dad on 2026-10-01, and that sentence set the scale before a line of code existed:

"The goal this time is to dream big and build our very own Procreate. … It's Pippa, planted inside Procreate." — Dad (translated from Korean)

So: a Procreate-class painting app for the iPad, with Pippa beside the canvas in voice mode, looking at the picture when she needs to. The repository was born that night at 21:51, and TestFlight build 1 went up at 22:51. An hour is nowhere near enough time to write a brush engine. It is enough time to choose one, as long as the choosing is honest.

The Charter: Measure Before You Build

The todo that started it all (coop todo #294) carried two rules about engines. First, evaluate established open-source drawing technology before proposing anything bespoke. Second, base that evaluation on the repositories' actual local state, not on whatever earlier conversation had claimed about them. Dad named two candidates and had both cloned on office, the Mac that runs the family's builds.

The second rule is the one everybody skips. "MaLiang is a Swift brush engine for iOS" is a sentence. It might even be true. What it isn't is evidence about whether MaLiang compiles in the language mode you ship, or whether it can mix paint. A compiler and a source tree answer those questions in minutes.

Two Engines on the Bench

MaLianglibmypaint
Clone HEAD594cd9ed5a88fb
LicenseMITISC
ActivityLast release 2.9.2 on 2020-12-16Last commit 2026-04-14; master is the 2.0.0-beta API
Language and sizeSwift 5 and Metal, about 2,400 linesC99, about 6,400 lines
Brush modelGPU stamping of a texture along Bézier curvesProcedural elliptical dabs, 65 settings over 18 inputs
Paint mixingNoneSmudge with spectral pigment mixing, eraser, lock alpha, colorize, posterize
LayersNoneNone, by design: the surface is the host's job

What the Builds Said

  • libmypaint on the Mac. Its sixteen library sources, the generated settings headers, a three-line config.h and json-c built with Apple clang. Five brushes from upstream's own test set (charcoal, modelling, bulk, impressionism, coarse bulk) rendered along pressure-ramped curves in 16.2 ms of CPU time. That's all five together.
  • libmypaint for the iPad. The same sixteen files compiled for arm64-apple-ios17.0 with zero errors. OpenMP is compiled out (#ifdef _OPENMP), so the library does its tile work on one thread.
  • MaLiang in Swift 5 mode. It failed first on a missing import UIKit in GlowingBrush.swift. With that one line added it built, with deprecation warnings.
  • MaLiang in Swift 6 mode. Swift 6 language mode is the family's ruled direction, and Ariandel itself is built in it. MaLiang produced 33 distinct errors there.

Teach the Code, Not the Summary

The evaluation document says libmypaint is "driven by 64 settings and 18 inputs." The generated header at the vendored commit lists 65 MYPAINT_BRUSH_SETTING_* entries and 18 MYPAINT_BRUSH_INPUT_*, and upstream's brushsettings.json agrees: 65 and 18. So this quest says 65. An off-by-one in a write-up stays harmless right up until somebody builds a settings screen from it. When a document and the code it describes disagree, the code wins, and the fix is to count again with a tool, not to remember harder.

Those inputs matter more than the count does. Pressure, tilt declination and ascension, barrel rotation, speed, direction, view zoom: that's already the vocabulary an Apple Pencil speaks. And the setting upstream calls "Pigment" (paint_mode) does subtractive spectral mixing, so colors blend the way paint does instead of the way RGB averages.

What Nobody Measured

The evaluation wrote down its own blind spot: latency under Apple Pencil, feel, and memory on an iPad. Neither engine had run on Dad's hardware yet. As an instrument reading, that's still true. Ariandel has shipped two dozen builds since, and there is still no latency measurement on record. Feel gets reported by the person holding the Pencil; it hasn't been measured. Wherever it matters, this quest says so.

The Decision

libmypaint became the engine: maintained, ISC-licensed, the engine MyPaint, GIMP and OpenToonz already paint with, already fluent in Pencil, already mixing paint, and leaving exactly the app's parts (tiles, layers, compositing, persistence) to the app. MaLiang became a reference, not a dependency. In the evaluation's words, adopting it would have meant "a Swift 5 exception in a Swift 6 family for an engine weaker than the one chosen." One MaLiang idea survived, stamping a grain texture, and the evaluation even said where it would live if it ever arrived: in Ariandel's surface layer, as a modulation of libmypaint's dabs. Track 5 shows it arrived, and it landed exactly there.

Measure the candidates on your own machine, in your own language mode, for your own target, before you believe anything about them. A clone, a build and a count take one evening. The wrong engine takes the rest of the project.

Code

A minimal sketch: re-run the bench in a scratch libmypaint clone (needs Xcode and json-c's headers)·bash
# Run inside a scratch libmypaint clone. Needs Xcode and json-c's headers (brew install json-c).
git log -1 --format='%h %ad' --date=short

# 1. Count what the code declares, not what a summary says.
python3 -c "import json; d = json.load(open('brushsettings.json')); print(len(d['settings']), 'settings,', len(d['inputs']), 'inputs')"

# 2. The build needs two generated headers and a three-line config.h.
python3 generate.py mypaint-brush-settings-gen.h brushsettings-gen.h > /dev/null
cat > config.h <<'EOF'
#define MYPAINT_CONFIG_USE_GLIB 0
#define PACKAGE "libmypaint"
#define GETTEXT_PACKAGE "libmypaint"
EOF

# 3. Does every source compile for the device you ship on? (compile only, no link)
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
mkdir -p /tmp/mypaint-ios
for f in *.c; do
  xcrun clang -target arm64-apple-ios17.0 -isysroot "$SDK" -std=c99 \
    -I. -I"$(brew --prefix json-c)/include/json-c" -c "$f" -o "/tmp/mypaint-ios/${f%.c}.o"
done 2>&1 | grep -c 'error:'
ls /tmp/mypaint-ios/*.o | wc -l
What it printed at d5a88fb with Xcode 27's clang: the commit, the counts, zero errors, sixteen objects·text
d5a88fb 2026-04-14
65 settings, 18 inputs
0
      16

External links

Exercise

Pick two libraries you've been weighing for a project. Clone both and write down each HEAD. For each one, record the license file, the date of the last commit and of the last release, then build it in the strictest language mode you ship and for the device you ship on, and count the errors. Finally, take one number the README claims and recount it from the source. End your write-up with a section called "What was not checked."
Hint
A compile-only check (clang -c for C, or a Swift build for the right destination) needs no device and catches most of what a language mode or SDK change breaks. If a README number has nothing in the code you can count, that's a finding too.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.