Skip to content

Add DrawMask: paint one colour through a coverage mask - #15

Merged
tannevaled merged 1 commit into
mainfrom
feat/mask-primitive
Aug 11, 2026
Merged

Add DrawMask: paint one colour through a coverage mask#15
tannevaled merged 1 commit into
mainfrom
feat/mask-primitive

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

A rasterised glyph is not an image. It carries no colour of its own, only how much of each pixel the outline covers — and text is the one thing every widget draws. A font back-end had to walk that mask calling PutPixel, scaling the coverage by the ink's alpha itself on every pixel, and PutPixel shifts, bounds-tests, clip-tests and blends one at a time.

ns/op
BenchmarkDrawMask 306,043
BenchmarkMaskPerPixel 700,334

2.3× on 2000 glyph-sized masks, the order a full window of labels issues.

Note what that number is not. There is no row to copy here: a mask has different coverage at every pixel, so unlike FillRect (41×) or DrawImage (6.7×) this cannot skip work, only stop repeating decisions. Two thirds of the gain came from writing the blend out instead of calling blendInto — 561,941 with the call, 306,043 without.

The hand-written blend is the risk, and it bit

Matching blendInto's colour rounding but not its alpha byte — which truncates where the colour bytes round — passed nine hand-picked cases, because every one of them had an opaque ground, where both formulas agree.

TestDrawMaskBlendsExactlyLikePutPixel sweeps all 256 coverages against five ink alphas and six grounds, three of them translucent. Reintroducing the wrong formula makes it fail at coverage 2, byte 3, by exactly one level — so the test is known to catch what it was written for, not merely known to pass.

The other half of the point

A font back-end can now ask for a capability. go-widgets/toolkit asserts *painter.PixelPainter — a concrete type — to render TrueType at all, so any other raster back-end silently falls back to the crude Text primitive. MaskPainter is what lets a recording, damage-tracking or future GPU painter receive real glyph coverage.

100% statement coverage.

🤖 Generated with Claude Code

A rasterised glyph is not an image. It carries no colour of its own, only
how much of each pixel the outline covers -- and text is the one thing
every widget draws. A font back-end had to walk that mask calling
PutPixel, scaling the coverage by the ink's alpha itself on every pixel,
and PutPixel shifts, bounds-tests, clip-tests and blends one at a time.

  BenchmarkDrawMask       306043 ns/op
  BenchmarkMaskPerPixel   700334 ns/op

2.3x on 2000 glyph-sized masks, the order a full window of labels issues.

Note what the number is NOT. There is no row to copy here: a mask has
different coverage at every pixel, so unlike FillRect (41x) or DrawImage
(6.7x) this cannot skip work, only stop repeating decisions. Two thirds
of the gain came from writing the blend out instead of calling blendInto
-- 561941 with the call, 306043 without.

That hand-written blend is the risk, and it bit: matching blendInto's
colour rounding but not its ALPHA byte, which truncates where the colour
bytes round, passed nine hand-picked cases because every one of them had
an opaque ground. TestDrawMaskBlendsExactlyLikePutPixel sweeps all 256
coverages against five ink alphas and six grounds, three of them
translucent, and fails on the wrong formula at coverage 2 by exactly one
level.

The other half of the point is that a font back-end can now ask for a
CAPABILITY. go-widgets/toolkit asserts *painter.PixelPainter -- a
concrete type -- to render TrueType at all, so any other raster back-end
silently falls back to the crude Text primitive. MaskPainter is what lets
a recording, damage-tracking or future GPU painter receive real glyph
coverage.

100% statement coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 302be58 into main Aug 11, 2026
1 check passed
@tannevaled
tannevaled deleted the feat/mask-primitive branch August 11, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant