Steps to reproduce
This is a code-level bug in the Windows embedder's resize synchronization, reproducible whenever an empty frame is generated while a window resize is pending (ResizeState::kResizeStarted). The most common real-world trigger is the (very popular) hidden-window startup choreography:
- Create the top-level window hidden (e.g. the
window_manager package's waitUntilReadyToShow flow, or a custom runner that shows the window only after first content).
- While the window is hidden and the Flutter scene is still empty/transparent (e.g. a splash phase), resize the window (
SetWindowPos / windowManager.setBounds / setTitleBarStyle, which changes the client area via WM_NCCALCSIZE).
- If the engine generates an empty frame during the 100 ms resize-synchronization window, the render surface is resized to transposed dimensions (height ↔ width).
- Show the window. All subsequent (correctly sized) frames are rasterized into the transposed surface, so the window displays horizontally stretched content with a black band — persisting until the next window resize.
Because it depends on an empty frame landing inside the ~100 ms kResizeStarted window, the failure is intermittent — in our production app (Otzaria) it reproduced on ~1 out of 10 cold launches, on Windows 11, single monitor, 100 % scaling (no DPI changes involved).
The bug
engine/src/flutter/shell/platform/windows/flutter_windows_view.cc, in OnEmptyFrameGenerated() — present in 3.44.0 stable and current master:
https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc#L155
bool FlutterWindowsView::OnEmptyFrameGenerated() {
std::unique_lock<std::mutex> lock(resize_mutex_);
...
if (resize_status_ != ResizeState::kResizeStarted) {
return true;
}
if (!ResizeRenderSurface(resize_target_height_, resize_target_width_)) { // <-- arguments transposed
return false;
}
resize_status_ = ResizeState::kFrameGenerated;
return true;
}
Compare with the correct call in OnFrameGenerated() a few lines below (line 190):
if (!ResizeRenderSurface(resize_target_width_, resize_target_height_)) { // correct order
ResizeRenderSurface is declared as (size_t width, size_t height), so the empty-frame path resizes the surface to height × width instead of width × height.
Why the corruption persists
After the transposed resize, resize_status_ advances to kFrameGenerated → kDone. From that point on, OnFrameGenerated() takes the early-return path (resize_status_ != kResizeStarted → return true) without ever validating or fixing the surface size, so every subsequent correctly-sized frame is drawn into the transposed surface. The only thing that heals it is the next WM_SIZE (e.g. the user manually resizing the window), which starts a fresh resize cycle.
Visual signature (how we confirmed the root cause)
With a transposed surface, the swapchain buffer is h × w inside a w × h client area. DWM stretches it to the window, producing:
- a horizontal stretch factor of
w / h (≈ 1.78 for 16:9), and
- a black band whose height is
(w − h) / w of the window height (≈ 43.7 % for 16:9).
We measured screenshots of the corrupted state on two unrelated machines (different resolutions, 100 % and 125 % scaling): the black band is 44 % of the window height on both, matching the prediction exactly.
| state |
screenshot measurement |
predicted by transposition |
| 1264×711 client (16:9-ish) |
black band ≈ 44 % of height, content stretched horizontally |
43.7 %, ×1.78 |
Expected results
The empty-frame path resizes the render surface to the resize target — ResizeRenderSurface(resize_target_width_, resize_target_height_) — identical to the non-empty-frame path.
Actual results
The surface is resized to transposed dimensions and stays transposed until the next window resize; the app appears "zoomed"/stretched with a large black band.
Proposed fix
One-line change — swap the arguments at flutter_windows_view.cc:155 to match OnFrameGenerated():
if (!ResizeRenderSurface(resize_target_width_, resize_target_height_)) {
Workaround for affected apps
In the runner, immediately before revealing the hidden window, jiggle the Flutter child HWND by 1 px and back (two MoveWindow calls). This forces a fresh resize cycle that restores the correct surface size. Verified across 51 cold launches with zero recurrences (vs. ~1/10 before).
Flutter Doctor output
flutter --version
Flutter 3.44.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 559ffa3f75 (4 weeks ago) • 2026-05-15 14:13:13 -0700
Engine • hash fcf463a2242790d1fdcd9d044f533080f5022e18 (revision 4c525dac5e) (27 days ago) • 2026-05-15 19:00:04.000Z
Tools • Dart 3.12.0 • DevTools 2.57.0
OS: Windows 11 Pro 10.0.26200, x64. Reproduced on two machines (100 % and 125 % display scaling, single monitor).
Steps to reproduce
This is a code-level bug in the Windows embedder's resize synchronization, reproducible whenever an empty frame is generated while a window resize is pending (
ResizeState::kResizeStarted). The most common real-world trigger is the (very popular) hidden-window startup choreography:window_managerpackage'swaitUntilReadyToShowflow, or a custom runner that shows the window only after first content).SetWindowPos/windowManager.setBounds/setTitleBarStyle, which changes the client area viaWM_NCCALCSIZE).Because it depends on an empty frame landing inside the ~100 ms
kResizeStartedwindow, the failure is intermittent — in our production app (Otzaria) it reproduced on ~1 out of 10 cold launches, on Windows 11, single monitor, 100 % scaling (no DPI changes involved).The bug
engine/src/flutter/shell/platform/windows/flutter_windows_view.cc, inOnEmptyFrameGenerated()— present in 3.44.0 stable and currentmaster:https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc#L155
Compare with the correct call in
OnFrameGenerated()a few lines below (line 190):ResizeRenderSurfaceis declared as(size_t width, size_t height), so the empty-frame path resizes the surface toheight × widthinstead ofwidth × height.Why the corruption persists
After the transposed resize,
resize_status_advances tokFrameGenerated→kDone. From that point on,OnFrameGenerated()takes the early-return path (resize_status_ != kResizeStarted → return true) without ever validating or fixing the surface size, so every subsequent correctly-sized frame is drawn into the transposed surface. The only thing that heals it is the nextWM_SIZE(e.g. the user manually resizing the window), which starts a fresh resize cycle.Visual signature (how we confirmed the root cause)
With a transposed surface, the swapchain buffer is
h × winside aw × hclient area. DWM stretches it to the window, producing:w / h(≈ 1.78 for 16:9), and(w − h) / wof the window height (≈ 43.7 % for 16:9).We measured screenshots of the corrupted state on two unrelated machines (different resolutions, 100 % and 125 % scaling): the black band is 44 % of the window height on both, matching the prediction exactly.
Expected results
The empty-frame path resizes the render surface to the resize target —
ResizeRenderSurface(resize_target_width_, resize_target_height_)— identical to the non-empty-frame path.Actual results
The surface is resized to transposed dimensions and stays transposed until the next window resize; the app appears "zoomed"/stretched with a large black band.
Proposed fix
One-line change — swap the arguments at
flutter_windows_view.cc:155to matchOnFrameGenerated():if (!ResizeRenderSurface(resize_target_width_, resize_target_height_)) {Workaround for affected apps
In the runner, immediately before revealing the hidden window, jiggle the Flutter child HWND by 1 px and back (two
MoveWindowcalls). This forces a fresh resize cycle that restores the correct surface size. Verified across 51 cold launches with zero recurrences (vs. ~1/10 before).Flutter Doctor output
flutter --version
OS: Windows 11 Pro 10.0.26200, x64. Reproduced on two machines (100 % and 125 % display scaling, single monitor).