Skip to content

Resize the renderer when a minimized window is restored on Windows - #15

Closed
chris-addison wants to merge 37 commits into
unity/hub-consolidatedfrom
unity/HUB-8360-restore-resizes-renderer
Closed

chris-addison wants to merge 37 commits into
unity/hub-consolidatedfrom
unity/HUB-8360-restore-resizes-renderer

Conversation

@chris-addison

@chris-addison chris-addison commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

On Windows, a window restored from the taskbar at a different size than it was minimized at drew only part of its client area. Found in the native Unity Hub (HUB-8360, Hub 4.0 beta feedback): after restoring, the right side was cut off, including the close button, and old pixels showed in the gap.

  • Before: the first WM_SIZE after SIZE_MINIMIZED restored the frame callback but skipped the renderer resize. The comment said the swap chain would be recreated through update_drawable_size_even_if_unchanged, which does not exist. Layout took the new size, but the swap chain kept the old one until the next manual resize. This happens when the display scale or resolution changes while the window is minimized, a monitor sleeps, a laptop docks or undocks, or the window restores to maximized.
  • Now: every WM_SIZE that is not a minimize resizes the renderer. Both renderers return early when the size is unchanged, so a plain restore costs nothing. handle_size_change loses its should_resize_renderer flag, since every caller now passes true.

Upstream gpui-ce main and zed have the same code, and neither has a fix. This also covers a minimized window shown again through Window::set_visible(true) (gpui-ce#239), which restores it through the same branch.

Testing

platform::tests::restoring_a_minimized_window_resizes_the_renderer opens a real window with a frame callback, minimizes it, grows its restore rectangle while it is minimized, restores it, and checks that the renderer's size matches the client rectangle. Run on a Windows desktop session:

  • Without the fix it fails: the renderer stays at 2848x1648 while the client area is 3048x1748.
  • With the fix it passes, and the full gpui_ce_windows lib suite passes (24 tests, test-support,render-to-image).

It needs a desktop session, like the existing directx_renderer::tests. In session 0 (for example over SSH) DXGI refuses the swap chain with 0x887A0022.

A second commit fixes CI that was already broken on unity/hub-consolidated: inline_reflow.rs still called assert_document_contract, which gpui-ce#338 renamed to assert_document_structure, so every build step failed, and gpui_wgpu's surfaces/windows.rs needed cargo fmt. Workspace clippy and the gpui_ce_parley lib tests pass locally.

This branch is unity/hub-consolidated, the Hub's current pin, plus these two commits.

🤖 Generated with Claude Code

francois-unity and others added 30 commits October 14, 2026 01:45
core-foundation = "=0.10.0" in the workspace dependencies forces every
downstream crate that depends on gpui to resolve exactly 0.10.0. Because
0.10.0 and 0.10.1 are semver-compatible, cargo picks one version per
compatible range, so a consumer that needs 0.10.1 (or pins any other
0.10.x) fails to resolve instead of getting a second copy.

A caret range keeps the same major/minor floor and lets the consumer's
lockfile choose the patch. This repo's own Cargo.lock is unchanged and
still resolves 0.10.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ws 0.61

- image: disable default features and enable only the formats gpui's
  ImageFormat maps (bmp, gif, ico, jpeg, png, pnm, tiff, webp) plus rayon.
  The default set pulled the ravif/rav1e AVIF encoder (~28 crates) that
  nothing calls, and the exr/dds/hdr/qoi/tga/ff decoders.
- bindgen: drop gpui's macOS build-dependency, which build.rs never used,
  and gate gpui_media's to macOS, the only target its build.rs generates
  bindings for.
- postage: optional behind test-support (and a dev-dependency). Only
  app/test_context.rs uses it, and that module is test-support-only.
- windows-capture 1.5 -> 2.0.1, which moves it to windows 0.62, so the
  windows-061 alias goes and gpui, gpui_wgpu and gpui_windows use the
  workspace windows crate. Frame::timestamp() now returns a Result and
  as_raw_texture() is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- wgpu 29 -> 30.0.1 and naga 29 -> 30.0.1. API changes: surfaces take a
  color_space (SurfaceColorSpace::Auto, wgpu's pre-30 behavior), adapter
  requests take apply_limit_buckets (false), present() moved to
  Queue::present, metal texture_from_raw takes a drop callback (None) and
  create_texture_from_hal takes the initial state (UNINITIALIZED, what 29
  hard-coded).
- accesskit 0.24 -> 0.25.1, with accesskit_consumer 0.39.1,
  accesskit_macos 0.27.1, accesskit_unix 0.24.0 and accesskit_windows
  0.35.1, so every platform adapter shares one consumer. Tree is renamed
  TreeInfo.
- wgsl-rs: drop the workspace-wide validation feature, which only backs
  macro-generated #[cfg(test)] tests but linked wgsl-rs's own naga 29 into
  every consumer's build. gpui_render enables it as a dev-dependency, and
  its subpixel test validates with the workspace naga directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- rand 0.9 -> 0.10.3 (workspace and gpui_sum_tree). The extension
  methods moved from Rng to RngExt, so three imports change.
- getrandom 0.3 -> 0.4.3 for gpui's wasm entropy, matching what rand
  0.10, tempfile and uuid already resolve.
- flume 0.11 -> 0.12, which drops nanorand and flume's spin 0.9 use.
- which 6 -> 8 in gpui_ce_util and gpui_zed_util, which drops rustix
  0.38, linux-raw-sys 0.4, home and winsafe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`CreateTargetForHwnd(hwnd, true)` puts gpui's visual tree on top of every
child window of the gpui window. gpui creates no child HWNDs of its own, so
this only bites an embedder that parents a native child, such as a wry /
WebView2 webview built with `build_as_child`: gpui's opaque frame covers the
child completely, while the child still receives mouse input and keeps
running. A video embed therefore plays audio with no picture.

`topmost: false` composes gpui's tree behind its children, which matches
macOS, where a child `NSView` draws above gpui's Metal layer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
wgpu 30 made `BufferSlice::get_mapped_range` return a `Result`. Upstream's
offscreen readback in `gpui_wgpu`'s `test-support` path (`headless.rs`) still
treated it as infallible, which only shows with `test-support` enabled. Map
the error the way the surrounding readback steps already do.
`render-to-image` (HUB-7425) compiles `Window::render_to_image` outside
`test-support`, and `gpui_macos`'s window forwards it to whichever renderer is
active. Upstream's macOS WGPU renderer (gpui-ce#287) only provides
`render_to_image` under `test-support`, because WGPU's offscreen readback
needs `gpui_wgpu`'s `test-support`. So `wgpu` plus `render-to-image` failed to
compile. Widen the renderer's existing "needs test-support" stub to cover
`render-to-image`, so that combination builds and reports the gap at runtime
rather than pulling in test-support's frame scheduling, which
`render-to-image` exists to avoid.
Lets an element publish the URL it links to as the AccessKit `url`
property, which is what a web page exposes for an `<a href>`. Like the
other `aria_*` passthroughs it only describes the element: opening the
URL stays with the element’s own click handler.
Mutter creates its window (and GNOME Shell matches it to a .desktop entry)
as soon as it handles xdg_surface.get_toplevel. The app_id was only sent
after the wgpu renderer was created, which flushes the connection and takes
100+ ms, so for that window GNOME showed the app as a separate generic
"gear" icon in the dock that vanished once the app_id arrived. Set the
title and app_id right after get_toplevel so they land in the same flush.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the Parley migration, line breaking lives in the platform text
system, and TestTextSystem lays every string out as one line: no soft
wrapping and no break at a newline. Before, gpui wrapped lines itself on
top of any text system, so headless tests could check that text wraps,
clamps, or hit-tests across rows. They can't any more.

This keeps the existing one-line layout, unchanged, for text that has no
newline and fits its wrap width. Otherwise each hard line starts a row,
and rows wrap greedily at whitespace with the same fixed glyph advances.
A word wider than the wrap width overflows its row, matching Parley's
default overflow-wrap, and line_clamp wraps only the first n - 1 rows,
matching how the Parley backend clamps. Inline layout without embedded
boxes gets the same rows. Caret, hit-testing, selection and vertical
movement work per row.

One elements::text test compared the measured text size with the root's
bounds, which only held while the text could not wrap. It now checks the
height and that the width fits.
Parley’s line breaker hangs a space that overflows a row and then
breaks, so a space that ends the paragraph leaves an empty row behind,
and that row counts in the layout height. A block’s max-content width
comes from Layout::width(), which leaves trailing whitespace out, so
laying the text out again at exactly that width always wraps it: a box
holding "Visit our " measured one row taller than the text it paints.
CSS hangs that whitespace instead.

After line breaking, a paragraph whose last rows hold only whitespace
after a soft break is now broken again: the earlier rows as before, and
the last one with no width limit, still aligned within the wrap width.
That covers several trailing spaces, whitespace-only text and clamped
layouts, and the empty row Parley also leaves after an oversized inline
box that ends a paragraph. A forced break (U+2028) still starts its own
row, and widths don’t change, since Layout::width() already left the
hanging whitespace out.
The previous commit hangs a paragraph’s closing whitespace on its last
row instead of wrapping it onto an empty one. A block still measured
its width with Layout::width(), which leaves trailing whitespace out,
so a fragment ending in a space now sat flush against the next one: in
a sentence built from fragments, "Visit our " followed by a link read
"Visit ourdownload archive". A text element’s width
(LineLayout::width) already keeps that space.

A block’s width now counts the whitespace that ends its paragraph when
it fits in the width the paragraph was given, so a block laid out
again at its own measured width keeps both its row and its space. When
it doesn’t fit, it still hangs and stops counting. Whitespace before a
forced break and the spaces a soft wrap hangs still don’t count.

The element test now builds that sentence as a baseline-aligned flex
row and checks both its row and its space, and a unit test pins when
the space counts.
`render-to-image` (HUB-7425) compiles `Window::render_to_image` outside
`test-support`, but no Linux window implemented it, so the trait’s default
answered “not implemented for this platform”. Linux windows draw through the
WGPU renderer, which already has an offscreen readback for visual tests: it
renders the scene into a texture, copies that to a buffer and maps it. That
readback was compiled only under `gpui_wgpu`’s `test-support`.

Give `gpui_wgpu` a `render-to-image` feature that compiles the readback alone
(`WgpuHeadlessRenderer` stays test-only), forward it from `gpui_linux` and
`gpui_platform`, and let the X11 and Wayland windows call it. The readback now
refuses while a window recovers a lost device, as the DirectX one does, rather
than panicking on the torn-down GPU resources.
The offscreen readback copies 8-bit rows and only swizzles `Bgra8Unorm`, which
held while it served the headless renderer alone. A window’s surface can pick
another format when it offers neither 8-bit UNORM one, so refuse those with an
error rather than returning swapped channels or a blank image.

Test the device-lost half of the guard too: the first test only covered
missing GPU resources, so dropping the `device_lost()` check kept it green.
The refusal now names both states, and the macOS `wgpu` stub’s comment no
longer claims the readback needs `test-support`.
TestAppContext::build hardcodes its platform: the stub TestTextSystem,
which paints no glyphs, no headless renderer, an empty asset registry,
and windows whose scale factor is the constant 2. A test can open a
window and read its scene, but not render it to pixels the way an app
draws it. HeadlessAppContext can, but it isn’t a TestAppContext, so
code written against TestAppContext and VisualTestContext can’t use it.

TestAppContext::with_platform takes a text system, an asset source, a
headless renderer factory and a scale factor. TestPlatform keeps the
scale factor and hands it to each TestWindow, which reports it from
scale_factor(), so Window::render_to_image returns an image of the
window’s size times that factor. build, and with it #[gpui::test],
keeps the stub text system, no renderer, no assets and a scale of 2.

gpui_platform::current_headless_renderer now returns WGPU’s headless
renderer on Linux, FreeBSD and Windows under test-support, through an
optional gpui_wgpu dependency that Linux already builds for its
windows. Under bench-support alone it still returns None there.

Unit tests drive the new constructor with a recording renderer. A
rendering test in gpui_platform renders IBM Plex through Parley with
the platform’s headless renderer at two scales, and CI runs it on all
three platforms.
DirectWrite answers fontique's script fallback with a single face, mapped
from one sample character with no locale. On an English system that pick
can be a Japanese or Korean UI font, so a Simplified-only character such
as 简 has no candidate and shapes as .notdef.

`ParleyTextSystem::with_script_fallback_families` resolves the system's
own pick for a script first, then appends the installed families it is
given. The Windows text system uses it to add the Chinese, Japanese and
Korean UI faces, plus SimSun-ExtB, after DirectWrite's Han pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since upstream gpui-ce#232 (8ffa9a7), build.rs checks the target at run
time, so every host compiles it, and it names bindgen unconditionally.
3edff57 (“Trim unused dependencies”) moved bindgen under a
macOS-only build-dependencies table, so on a Linux or Windows host the
build script fails with E0433. That reddens clippy, msrv, test-linux and
test-windows on every PR into this branch. Back to upstream’s plain
[build-dependencies], with a comment saying why it stays unconditional.
WgpuDeviceRequirements gains an experimental_features token that
create_device passes to request_device when the application opted in and
the merged required features include an EXPERIMENTAL_* one (for example
EXPERIMENTAL_RAY_QUERY). The default keeps experimental features disabled,
so existing callers create the same device as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e#287)

- gpui_windows: opt-in `wgpu` feature swaps the DirectX renderer for a thin
  WindowsWgpuRenderer presenting to the HWND via wgpu surface; DirectX stays
  the default and remains compiled (direct_write shares its internals)
- gpui_wgpu: new_for_raw_window_handle / recover_raw_window_handle for the
  Win32 HWND path (instance carries a synthesized Windows display handle),
  custom-gpu texture compositing in surfaces/windows.rs, cfg gates for
  WgpuContextHandle::from_window / WgpuRenderTarget::surface
- gpui: SurfaceSource::Texture + gpu_context_info available on Windows
  behind gpui/custom-gpu
- gpui_platform: windows-wgpu feature
- custom_gpu example: enable on Windows; give the surface element an explicit
  size (a 0x0 layout bounds made the core drop the surface primitive)
Renders a backdrop quad plus an application WGPU texture through the full
frame path (draw_surfaces -> custom-gpu arm) with WgpuHeadlessRenderer and
asserts the composited pixels. Also exposes WgpuHeadlessRenderer::gpu_context
for tests that pre-fill application textures.
- WindowsWgpuRenderer flags skipped frames via needs_redraw; WindowsWindow
  propagates it into force_render_pending so the vsync pump schedules another
  frame — a lost surface or deferred recovery no longer leaves the window
  blank (Copilot review finding on gpui-ce#307)
- delegate new_for_raw_window_handle / recover_raw_window_handle to
  new_for_target / recover_target via NativeSurfaceTarget::Window, sharing the
  upstream construction/recovery flow; synthesize the Windows display handle
  the DXGI instance requires
- drop leftover debug prints from the surface paint path and the empty
  conditional in Scene (review findings)
- custom_gpu example: document the windows-wgpu run command
- rebase onto main (includes gpui-ce#287 macOS wgpu renderer): cfg gates now cover
  macOS and Windows side by side
Recovery clears the WGPU atlas, so sprite batches in a pre-recovery scene
reference tile ids that no longer exist and can panic in atlas lookup.
Return after every recovery attempt (success or failure) and let the
forced frame — requested via needs_redraw — rebuild the scene, matching
the macOS WGPU renderer's recovery flow.
With the wgpu feature, the platform still created DirectXDevices at
startup and the vsync thread polled them for device loss. The wgpu
renderer never draws with them; it owns its device and recovers from
losing it on the next draw. The spare D3D11 device sat on the GPU, and
if recreating it after a loss failed, the vsync thread panicked an app
whose rendering was fine.

The vsync thread now runs without devices (it only needs DwmFlush) and
skips the device-loss check when there are none. Window creation stops
carrying DirectXDevices under the feature, and the window's
WM_GPUI_GPU_DEVICE_LOST handler, which nothing sends any more, goes
with them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
render_to_image bailed with "not implemented". It now renders through
gpui_wgpu's offscreen path, as the macOS wgpu renderer does, and the
test-support feature turns on gpui_wgpu's test-support to provide it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requirements only take effect when the shared device is created, at the
first window. A later set_gpu_requirements was silently ignored; it now
logs a warning saying to call it before opening a window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- with the wgpu feature the text system is ParleyTextSystem with the
  DX11-free swash rasterizer (SwashGlyphRasterizer), and no DirectX
  devices are created; the vsync thread then only paces frames
- the vsync device-lost watch and handle_gpu_device_lost stay
  DirectX-renderer-only
- begin_vsync_thread mutability fix after the cfg split
The fork's render-to-image feature compiles Window::render_to_image without
test-support (the Hub's e2e screenshots). With the wgpu feature the window's
renderer is the WGPU one, whose render_to_image was gated on test-support
alone, so a render-to-image build without test-support did not compile.
render-to-image now forwards to gpui_wgpu's render-to-image and enables the
WGPU renderer's readback. Also joins a log message that a lost line
continuation had split by a run of spaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two test literals predate ContentMask's fade_out field (gpui-ce#298), and
the custom_gpu example predates wgpu 30's optional vertex buffer layouts,
so cargo test -p gpui_ce_wgpu stopped compiling. Both now fill the new
shapes with their defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WgpuHeadlessRenderer::gpu_context_info returns the WgpuContextHandle a
window's renderer would hand a custom-GPU control through
WgpuContextHandle::from_window. A headless window has no platform renderer
to ask, so a test that drives a texture-surface control headlessly can now
give it the same device the frame is composited on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chris-addison and others added 5 commits October 14, 2026 01:46
…_PREF

WgpuHeadlessRenderer::new_with_requirements passes the features and limits
an application would hand a window's renderer, so a headless capture of a
custom-GPU control runs on a device that can do what the window's can.
The headless adapter request still prefers low power, and now honours
WGPU_POWER_PREF, so a dual-GPU machine can measure the discrete GPU the
window would get.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wgpu 30's create_texture_from_hal takes the wrapped resource's initial
state. The fork's wgpu 30 upgrade passed it on macOS but missed the
Windows capture path behind wgpu-surfaces, which nothing built until the
Windows WGPU renderer turned it on. It passes UNINITIALIZED, what wgpu 29
always assumed, as the macOS path does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`WgpuDeviceRequirements::preferred_backend` names the native backend to try
first when gpui creates a window's (or a headless renderer's) device; the
platform's other backends follow in their usual order, so a machine without
it still gets a device. An application that wants Vulkan on Windows, where
ray queries on Direct3D 12 also need DXC, asks for it here. `None` (the
default) keeps the platform's order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`WindowOptions::windows_renderer` (`WindowsRenderer::DirectX`, the default,
or `WindowsRenderer::Wgpu`) picks each Windows window's renderer. The `wgpu`
feature now compiles the WGPU renderer in beside Direct3D 11 instead of
replacing it for every window, so an application can draw one window with
WGPU (and lend its device to a custom GPU control) while the rest keep
Direct3D 11 and DirectComposition. Without the feature a `Wgpu` window logs
a warning and draws with DirectX.

Text stays DirectWrite under both renderers: the platform keeps the
DirectWrite text system (its glyphs are rasterized on the CPU, so they
upload to either atlas), and a WGPU window applies DirectWrite's gamma and
contrast, so its text matches a DirectX window's. The DirectX devices and
the vsync thread's device-lost watch are back for `wgpu` builds; a WGPU
window ignores the DirectX device-lost message and recovers its own device.

The custom_gpu example opens its window with `WindowsRenderer::Wgpu`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restoring a window from the taskbar skipped the renderer resize, so a window
restored at a different size than it was minimized at (a display, scale or
resolution change while minimized, or restoring to maximized) kept its swap
chain at the old size. Layout used the new size, so the right and bottom of
the window were cut off until the next manual resize. Always resize: both
renderers return early when the size is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chris-addison
chris-addison force-pushed the unity/HUB-8360-restore-resizes-renderer branch from 3759e0d to 87792a4 Compare October 17, 2026 21:13
…ting

inline_reflow.rs still called assert_document_contract, which the upstream
refactor (gpui-ce#338) renamed to assert_document_structure, so every build step
failed. surfaces/windows.rs needed cargo fmt. Neither is part of the restore
fix; both were already broken on unity/hub-consolidated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chris-addison
chris-addison force-pushed the unity/hub-consolidated branch from 2e8f5ed to 8bd155e Compare October 17, 2026 21:29
…ologies/gpui-ce into unity/HUB-8360-restore-resizes-renderer

# Conflicts:
#	crates/gpui_wgpu/src/wgpu_renderer/surfaces/windows.rs
#	crates/gpui_windows/src/wgpu_renderer.rs
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.

5 participants