Skip to content

Paint Windows image panes from a cached composite, and decode off the UI thread - #896

Merged
SimonCropp merged 1 commit into
mainfrom
fix-image-perf
Sep 23, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
fix-image-perf

Conversation

@SimonCropp

Copy link
Copy Markdown
Member

Fixes the Windows image pane perf item from todo.md.

Paint cost

Measured on a canvas showing a pair of 2000×1500 PNGs, calling OnPaint directly:

Before After
Every paint (wheel notch, resize, hover) 46–66 ms ~1 ms
First paint at a new size same as above ~50 ms, once per picture and size
Decode 77–102 ms, on the UI thread the same work, on the thread pool
  • Composite cache: ImageCache.Composite holds the checkerboard with the scaled picture over it, per picture and drawn size. It's disposed along with the decoded picture, so Keep still bounds it to what's on screen. A paint copies the composite; the Windows pixel baselines, Images included, are unchanged by this.
  • Background decode: ImageCache.Get(path, hash, loaded) decodes on the pool and hands the result back through the canvas's BeginInvoke. The decode starts as soon as a screen arrives, not at the first paint. A decode that finishes for a picture no longer on screen, or for an older version of the file, is discarded. The rows draw while it runs, and the picture follows. A capture has no later paint to wait for, so it still decodes synchronously (LoadPictures).

Footer buttons (found while adding the tests)

Adding any test to FormsHeadTests failed the InlineAccepted and Minimal pixel tests, on main too. The footer buttons are pooled and relabelled as the screen changes. At WinForms' default AutoSizeMode.GrowOnly, each button kept the width of the longest label it had ever held. So the baselines recorded whichever labels the run happened to show first, and in the app buttons stayed oversized after switching between entry types.

The buttons are now GrowAndShrink, with the default 75×23 as the minimum. Six Windows baselines are re-approved. Only their footers changed, and they now come out byte-identical in either test order.

Tests: ImageCacheTests.ADecodeWithSomewhereToPostItIsHandedBack, ADecodeForAPictureNoLongerOnScreenIsDropped, APictureIsComposedOncePerSize, and FormsHeadTests.RepaintingAPictureComposesItOnce, AFooterButtonIsSizedToItsCurrentLabel.

The full solution passes locally in Release: 2,157 passed, 24 skipped.

… UI thread

- The checkerboard and the scaled picture are composed once per picture and
  size (ImageCache.Composite) and copied on every paint after. Scaling a pair of
  2000 by 1500 pictures and drawing the checkerboard under them on every paint
  cost 46 to 66 ms a paint, on every wheel notch and resize; the copy costs
  about 1 ms. Composing costs about 50 ms, once.
- The window decodes on the pool and paints the picture when the decode is
  handed back through BeginInvoke. It started when the screen arrives, and a
  decode that finishes for a picture no longer on screen is dropped. The pair
  above took 77 to 102 ms to decode on the UI thread. A capture still decodes
  synchronously (LoadPictures), having no later paint to wait for.
- Footer buttons are sized to their current label (GrowAndShrink, with the
  default 75 by 23 as the minimum). The pool relabels them as the screen
  changes, and at GrowOnly each button kept the width of the longest label it
  had ever held, so the pixel baselines recorded the order the tests happened
  to run in: adding any test to FormsHeadTests failed InlineAccepted and
  Minimal. Six Windows baselines are re-approved; only the footers changed,
  and they now come out byte identical in either order.
@SimonCropp SimonCropp added this to the 20.5.0 milestone Sep 23, 2026
@SimonCropp
SimonCropp merged commit 4aa3c6c into main Sep 23, 2026
10 checks passed
@SimonCropp
SimonCropp deleted the fix-image-perf branch September 23, 2026 11:13
This was referenced Sep 23, 2026
This was referenced Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant