Skip to content

DrawImage: build a scaled row once, not once per repeat - #11

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

DrawImage: build a scaled row once, not once per repeat#11
tannevaled merged 1 commit into
mainfrom
feat/image-scaled-fastpath

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Migrating the toolkit's widgets onto DrawImage measured far less than the 7.5× the primitive shows on its own: 2.3× for Image, 1.5× for Wallpaper.

The reason is that a widget almost never blits 1:1 — a wallpaper or a photo in a panel is enlarged — and the row-copy fast path required the destination to be the same width as the source. Every scaled blit fell through to the per-pixel loop, so the primitive was fast exactly in the case widgets don't hit.

Enlarging draws several destination rows from one source row. Building that row once and copying it to its repeats turns the cost from the destination's height into the source's — which is the whole point of enlarging. Rows that are not repeats go through scaleRow, which samples without blending, the row being known opaque before either path is taken.

ns/op
BenchmarkDrawImageScaled 434,038
BenchmarkPerPixelBlitScaled 2,078,585

4.8× on a 500×350 image filling a 1000×700 window. The 1:1 case is unchanged.

Correctness

TestDrawImageScaledFastPathMatchesPerPixel compares the fast path against the per-pixel path byte for byte over four ratios — enlarged evenly, enlarged unevenly, shrunk, and wider-but-shorter. A row-repeat that reuses a row built under different conditions would show up immediately.

The repeat is only taken when the row is known opaque and unclipped, because a composited row depends on what was underneath it and cannot be reused.

100% statement coverage.

🤖 Generated with Claude Code

Migrating the toolkit's widgets onto DrawImage measured far less than the
7.5x the primitive shows on its own: 2.3x for Image, 1.5x for Wallpaper.
The reason is that a widget almost never blits 1:1 -- a wallpaper or a
photo in a panel is ENLARGED -- and the row-copy fast path required the
destination to be the same width as the source, so scaled blits fell
through to the per-pixel loop.

Enlarging draws several destination rows from ONE source row. Building
that row once and copying it to its repeats turns the cost from the
destination's height into the source's, which is the whole point of
enlarging. Rows that are not repeats are built by scaleRow, which samples
without blending -- the row is known opaque before either path is taken.

  BenchmarkDrawImageScaled        434038 ns/op
  BenchmarkPerPixelBlitScaled    2078585 ns/op

4.8x on a 500x350 image filling a 1000x700 window.

TestDrawImageScaledFastPathMatchesPerPixel compares the fast path against
the per-pixel path byte for byte over four ratios -- enlarged evenly and
unevenly, shrunk, and wider-but-shorter. 100% statement coverage.

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