Add DrawImage, a primitive for blocks of pixels - #10
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
Paintercould 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'sImage,Thumbnail,Wallpaper,Browser,ColorPickerand both font paths all loop over the destination callingPutPixel.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
Painterand nothing else — the concrete case being the news reader, which cannot move ontogo-widgets/windowuntil this exists.DrawImagetakes 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.
BenchmarkDrawImageBenchmarkPerPixelBlit7.5× on a full 1000×700 window — median of five 100-iteration samples, discarding the warm-up outliers.
Correctness
TestDrawImageRowCopyMatchesPerPixelblits 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
Bufshorter thanWidth*Height*4—PutPixeltolerates that, and the fast path must not be the one place that panics on it.CellPainterimplements the capability too, degrading to coloured cells throughPutPixel, keeping one definition of what a pixel means on a grid.100% statement coverage.
What this unblocks
go-widgets/window, deleting its duplicateinternal/window🤖 Generated with Claude Code