Title Brushes, Upside Down
Commit 139c64f (2026-10-03, 00:16 KST) gave Ariandel shaped tips: Procreate's Shape Source, a picture stamped where libmypaint asks for a round dab, through Ariandel's own draw_dab. Before the half hour was out, imported Procreate brushes set beside Procreate's own thumbnails showed the stamped title brushes upside down and mirrored. The coop learning that records it (#756, scope libmypaint) puts the symptom exactly: a tip whose picture sits in its top-left corner was stamped into the bottom-right corner.
"Upside down and mirrored" is worth slowing down on. Flip a picture vertically and horizontally and you have turned it half a turn. A single flip changes one axis; a half turn changes both. The description already contained the diagnosis.
The Wrong First Suspect
The first hypothesis was a vertical flip in decoding the picture: Core Graphics puts its origin at the bottom-left, and an image read into a buffer upside down is the oldest bug in the book. It was a reasonable suspect, and it was wrong. A round trip proved the decoded picture upright. A decode flip would also have mirrored only one axis, and the symptom had both.
Line 903
The cause was in libmypaint, and it was not a bug there. Before handing draw_dab its angle, mypaint-brush.c corrects it for the view's rotation (first code block):
ACTUAL_ELLIPTICAL_DAB_ANGLE = mod_arith(setting − viewrotation + 180, 180) − 180
That folds every angle into the range −180 up to (not including) 0. The default setting of 90° arrives as −90°. A setting of 0 and a setting of 180 arrive as the same number. The angle draw_dab receives is only known modulo half a turn.
For libmypaint's own dab that loses nothing. Its dab is an ellipse, and an ellipse turned half a turn is the same ellipse. The reduction is correct for the only shape the engine ever draws.
Ariandel's first shaped-tip code trusted that angle. It turned the tip by angle − rest_angle, with BrushShape.restAngle = 90 and a doc comment that read: "The angle libmypaint gives a dab when the brush does not turn it: the tip stands upright there." libmypaint gives −90. So an unturned brush turned its tip by −90 − 90 = −180°: exactly half a turn, exactly the symptom.
The Probe That Found It
The learning records the experiment that settled it, and it is a model of a good probe. Make a tip with only its top-left quarter white: an asymmetric shape where any rotation or flip is visible in one bit. Stamp it at (100, 100) with radius 30. Paint appeared at (120, 120), down and right of center, and none at (80, 80), up and left. One stamp, two pixels, and the half turn is beyond argument. The second code block rebuilds that probe from the formulas: libmypaint's mod_arith, the delivered angle, and the same tip-space sampling Ariandel's stamp() uses.
The Fix
Commit 963bef1 (00:43 KST) stopped asking libmypaint which way the tip points. The surface now discards the angle argument outright with (void)angle and builds the turn from sources that know the full circle (third code block):
- The tip's own angle,
BrushShape.angle: Procreate's Shape Angle, a slider in Brush Studio, set by the importer. - The stroke's heading, which the surface computes itself from successive dab positions with
atan2f: a full-circle direction, weighted byfollow. - The Pencil's azimuth, handed in per input through
ard_surface_set_azimuth, weighted byfollow_tilt. - A random turn up to
scatterof a half turn either way, from the stroke's own seed.
libmypaint still contributes the dab's ratio, which squeezes the tip along its height; a ratio has no direction, so nothing was lost there. Symmetry reflections pass a mirror flag and a reflected turn, so a mirrored dab carries a mirrored tip.
The code fix came first. The lesson was written down three and a half hours later, at 04:17, in two places at once: a line on the house sign (c9bd18e), and coop learning #756, which is where the probe's numbers live.
The Shape of the Mistake Elsewhere
This trap is not about painting. A unit quaternion q and its negation −q describe the same rotation, which is harmless until you interpolate between two of them and take the long way round. An undirected edge drops its direction, which is harmless until you need to walk the graph one way. A surface normal with an arbitrary sign is harmless for a two-sided material and wrong for a one-sided one. In each case the producer was right for its consumer, and a new consumer inherited a value that had quietly forgotten something.