Skip to content

Add DrawImage, a primitive for blocks of pixels - #10

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

Add DrawImage, a primitive for blocks of pixels#10
tannevaled merged 1 commit into
mainfrom
feat/image-primitive

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The Painter could draw rectangles, rounded rectangles, text and single pixels — and nothing that carried pixels of its own. Every widget showing an image had to spell it out one pixel at a time: the toolkit's Image, Thumbnail, Wallpaper, Browser, ColorPicker and both font paths all loop over the destination calling PutPixel.

Applications with a framebuffer of their own went further and bypassed the painter completely, reaching for the raw buffer. That is what stops them being hosted by a back-end that hands out a Painter and nothing else — the concrete case being the news reader, which cannot move onto go-widgets/window until this exists.

DrawImage takes the whole block: nearest-neighbour scaling — the same mapping the hand-written loops used, so output is unchanged — honouring the clip and the translation like every other primitive.

The measurement corrected the story

I expected the win to come from removing ~700,000 interface calls per full-window image. It does not. A first version that was just a per-pixel loop behind one call measured 1,784,333 vs 1,890,500 ns/op — 6%. The interface call is not what costs.

What pays is deciding per row instead of per pixel. When the destination is the same width as the source, sits on the surface, is unclipped and fully opaque, the row is copied wholesale; scanning the alphas to find that out costs a quarter of the copy and saves the blend on every pixel.

ns/op
BenchmarkDrawImage 251,797
BenchmarkPerPixelBlit 1,877,823

7.5× on a full 1000×700 window — median of five 100-iteration samples, discarding the warm-up outliers.

Correctness

TestDrawImageRowCopyMatchesPerPixel blits the same image twice, once down the fast path and once forced onto the per-pixel path by a covering clip, and compares every byte. The speed is worthless if the picture differs.

The fast path checks the buffer bound per row rather than up front, because a caller may hand over a Buf shorter than Width*Height*4PutPixel tolerates that, and the fast path must not be the one place that panics on it.

CellPainter implements the capability too, degrading to coloured cells through PutPixel, keeping one definition of what a pixel means on a grid.

100% statement coverage.

What this unblocks

  • the 7 toolkit widgets that hand-roll a blit today
  • the news reader's migration onto go-widgets/window, deleting its duplicate internal/window

🤖 Generated with Claude Code

The Painter could draw rectangles, rounded rectangles, text and single
pixels -- and nothing that carried pixels of its own. Every widget showing
an image had to spell it out one pixel at a time: the toolkit's Image,
Thumbnail, Wallpaper, Browser, ColorPicker and both font paths all loop
over the destination calling PutPixel. Applications with a framebuffer of
their own went further and bypassed the painter completely, reaching for
the raw buffer -- which is exactly what stops them being hosted by a
back-end that hands out a Painter and nothing else.

DrawImage takes the whole block: nearest-neighbour scaling, the same
mapping the hand-written loops used, so output is unchanged, and it
honours the clip and the translation like every other primitive.

The gain is in the fast path, not in saving interface calls -- the first
version, a plain per-pixel loop behind one call, measured only 6% better
than the loops it replaced. What pays is deciding per ROW instead of per
pixel: when the destination is the same width as the source, sits on the
surface, is unclipped and fully opaque, the row is copied wholesale.
Scanning the alphas to find that out costs a quarter of the copy and
saves the blend on every pixel.

  BenchmarkDrawImage        251797 ns/op
  BenchmarkPerPixelBlit    1877823 ns/op

7.5x on a full 1000x700 window, median of five 100-iteration samples.

CellPainter implements it too, degrading to coloured cells through
PutPixel, so one definition of what a pixel means on a grid.

100% statement coverage, including the short-Buf tolerance both paths
need and an equivalence test proving the fast path and the per-pixel path
produce identical bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 480bd13 into main Aug 11, 2026
1 check passed
@tannevaled
tannevaled deleted the feat/image-primitive branch August 11, 2026 09:20
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