Skip to content

Downscale oversized images before atlas upload - #19

Merged
chris-addison merged 2 commits into
unity/hub-consolidatedfrom
unity/HUB-0000-downscale-oversized-images
Oct 23, 2026
Merged

chris-addison merged 2 commits into
unity/hub-consolidatedfrom
unity/HUB-0000-downscale-oversized-images

Conversation

@toddbeauchamp

Copy link
Copy Markdown

Taude Taude:

Images drawn well below their pixel size now come out smooth instead of jagged. Found in the native Unity Hub on Windows: the editor version badges (512px PNGs drawn at 32px) and the account avatar looked pixelated.

  • Before: the sprite atlas has no mipmaps and every renderer samples it bilinearly, so a 512px image drawn at 32px read only the 2x2 texels nearest each screen pixel, 4 of every 256. Windows at 100% scale showed it worst; a Retina Mac hid most of it.
  • Now: Window::paint_image halves the frame on the CPU until it is within 2x of its device-pixel draw size, then uploads that copy. The filter is a 2x2 box weighted by alpha, so transparent pixels don't darken edges.
  • Bounded cache: the copy is keyed by a new downscale_level field on RenderImageParams, so an image keeps at most a few variants however its size changes during a resize or animation. drop_image frees all of them, and has_image_atlas_entry counts any of them.
  • Unchanged: images drawn at or near their own size upload exactly as before.

Rounding to whole halvings is a trade-off: the GPU still does the last step of up to 2:1, so a small icon can look slightly soft. Exact-size resampling would make a new atlas entry for every size an image passes through.

Screenshots

Hub visual scenes rendered headless at scale 1 on the same Mac, before (left) and after (right), zoomed.

Editor badge, Installs list:

Unity 6 editor badge at 32px, jagged before and smooth after

Legacy Unity cube, Welcome page:

Unity cube on the Welcome page, jagged before and smooth after

Template icon, New Project (the softest case):

Unity 6 badge at 20px in New Project, slightly soft after

Testing

  • New assets::tests cover the level choice (32px at 100%, 125%, 150% and 200% scale), odd sides, the one-pixel floor, a checkerboard averaging to grey, and alpha weighting.
  • New elements::img::tests: a 512px image drawn at 32px uploads a tile between 32 and 64px and drop_image frees it; an image drawn at its own size uploads the original.
  • cargo test --workspace, clippy (all targets) and cargo fmt --check pass on macOS. gpui_ce_windows type-checks for x86_64-pc-windows-msvc (all targets, render-to-image,test-support), with shader compilation stubbed; it was not run on Windows.
  • Hub against this commit: its image tests pass. Of 58 visual scenes, 40 are pixel-identical and 18 change only inside image boxes (editor badges, the template icon, the Welcome cube, the activity drawer badge). Those 18 baselines need updating when the Hub pin moves.

This branch is unity/hub-consolidated plus one commit. The Hub's current pin is b117a40da3 (#17 on top of this base), and the commit cherry-picks onto it cleanly.

🤖 Generated with Claude Code

The sprite atlas has no mipmaps and every renderer samples it bilinearly,
so an image drawn far below its pixel size reads only the 2x2 texels
nearest each screen pixel and skips the rest. A 512px editor badge drawn
at 32px on a 100% Windows display read 4 of every 256 source pixels and
came out jagged; Retina hid most of it at 8:1.

paint_image now picks a whole number of halvings that brings the frame
within 2x of its device-pixel draw size, box-filters a copy on the CPU
(alpha-weighted, so transparent edges do not darken) and uploads that.
RenderImageParams carries the level, so each image keeps at most a
handful of variants however its draw size changes, and drop_image frees
all of them. Images drawn at or near their own size upload as before.
@toddbeauchamp
toddbeauchamp marked this pull request as ready for review October 22, 2026 14:23

@chris-addison chris-addison left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claudistopher Claudistopher:

This takes the same approach as Chromium: upload a copy shrunk by whole halvings, then let the GPU do the last step of up to 2:1.

Check What I saw
✅ How many halvings Matches Chromium: stays at or above the draw size, and the axis shrunk less decides
✅ Cache key The halving count is part of the key, as in Chromium
✅ Edge blending Weighting by alpha matches Chromium's premultiplied averaging, so transparent edges don't darken
✅ Whole halvings, not exact size Right call. Chromium also switches to mipmaps when it shrinks an image
❌ Paint cost The shrink runs on the UI thread during paint. A large avatar could stall one frame the first time it's drawn at each size. Chromium does this on worker threads
❌ Extra copy downscale_bgra copies the whole source with to_vec() before the first halving. The first pass could read the borrowed slice directly

Not covered: Windows. Badges at 100% scale there, where the bug was reported, still need a check before the Hub moves its pin. An image shrunk by an element transform still aliases, but the Hub probably doesn't do that.

Approving: neither ❌ blocks this, and both can be follow-ups.

@toddbeauchamp

Copy link
Copy Markdown
Author

Taude Taude:

Thanks Chris. The extra copy is gone in 04cf99f, and I measured the paint cost: badges are cheap, but very large avatars can drop a frame.

Extra copy: the first halving now reads the borrowed source slice, so nothing is copied up front.

Paint cost: the first draw at each size, release build on an M5 Max, median of 41 runs:

Image Before 04cf99f After
512px badge at 32px (100% scale) 0.87 ms 0.72 ms
512px badge at 48px (150% scale) 0.75 ms 0.71 ms
1024px avatar at 32px 2.89 ms 2.87 ms
2048px avatar at 32px 11.65 ms 11.57 ms

Almost all the time is the filter loop, not the copy. A badge costs under a millisecond once, but a 2048px avatar takes about one 60 Hz frame, and slower Windows machines will take longer. Moving the shrink to a background thread will be a follow-up PR, as you suggested.

@chris-addison
chris-addison merged commit b80e321 into unity/hub-consolidated Oct 23, 2026
15 checks passed
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.

2 participants