Skip to content

FillRect fills rows, not pixels - #14

Merged
tannevaled merged 1 commit into
mainfrom
perf/fillrect-rows
Aug 11, 2026
Merged

FillRect fills rows, not pixels#14
tannevaled merged 1 commit into
mainfrom
perf/fillrect-rows

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Profiling a Wallpaper after moving it onto DrawImage showed 75% of its time in FillRect — not in the image at all. FillRect was a PutPixel per pixel, and a PutPixel is a translation, two bounds tests, a clip test and a blend: 700,000 of them to paint one window-sized background.

This is the primitive the toolkit leans on hardest. Every background, every button, every table row, every focus ring goes through it — so the cost was being paid by every widget on every frame, not by images. The image work was the smaller fish; it just happened to be the one holding the profiler.

Where a fill may write is a rectangle — the destination intersected with the surface and with the clip — and it is decided once. An opaque fill then writes the same four bytes everywhere, so one row is built and the rest of the rectangle is that row copied. The row itself is built by writing one pixel and doubling it, so N pixels cost log2(N) copies rather than N stores. A translucent fill still composites pixel by pixel, because its result depends on what was underneath.

FillRect per-pixel
full window (1000×700) 42,612 1,751,507 41×
35 table rows (180×18) 8,164 285,603 35×

The small-fill case is there because a table of rows and a row of buttons is what the toolkit actually issues, and the per-row set-up has to earn its keep at that size too.

Proving the picture did not change

TestFillRectMatchesTheLoopItReplaced compares against the PutPixel loop byte for byte over 18 cases: single pixel, single row, single column, odd widths, off each edge, entirely off the surface, empty, negative height, fully transparent, translucent, clipped, clipped away, translated, and translated and clipped. The ground is painted a non-black colour first, so a blend that wrongly behaves as a copy shows up instead of hiding in zeroes.

100% statement coverage.

🤖 Generated with Claude Code

Profiling a Wallpaper after moving it onto DrawImage showed 75% of its
time in FillRect -- not in the image at all. FillRect was a PutPixel per
pixel, and a PutPixel is a translation, two bounds tests, a clip test and
a blend: 700,000 of them to paint one window-sized background.

This is the primitive the toolkit leans on hardest. Every background,
every button, every table row, every focus ring goes through it, so the
cost was being paid by every widget on every frame, not by images.

Where a fill may write is a rectangle -- the destination intersected with
the surface and with the clip -- and it is decided once. An opaque fill
then writes the SAME four bytes everywhere, so one row is built and the
rest of the rectangle is that row copied. The row itself is built by
writing one pixel and doubling it, so N pixels cost log2(N) copies rather
than N stores. A translucent fill still composites pixel by pixel,
because its result depends on what was underneath.

  full window     42612 ns/op  vs  1751507 per-pixel   41x
  35 table rows    8164 ns/op  vs   285603 per-pixel   35x

TestFillRectMatchesTheLoopItReplaced compares against the PutPixel loop
byte for byte over 18 cases: single pixel, single row, single column, odd
widths, off each edge, entirely off the surface, empty, negative height,
fully transparent, translucent, clipped, clipped away, translated, and
translated AND clipped. The ground is painted a non-black colour first so
a blend that wrongly behaves as a copy cannot hide in zeroes.

100% statement coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit a6de2fc into main Aug 11, 2026
1 check passed
@tannevaled
tannevaled deleted the perf/fillrect-rows branch August 11, 2026 10:11
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