From bdadca88b1b4ca67f5eeb448e879cc4d823e5dfb Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Tue, 29 Sep 2026 14:37:43 -0700 Subject: [PATCH] macOS: ask for a fresh capture while a resize waits for its paint On macOS CEF paints by capturing the root frame at the size of the renderer's last activated frame. After a big resize (mid-way through an animated device switch, typically), the renderer's new-size frame activates and the capture size changes, but a page with nothing else changing gives the capturer no damage, and the capturer drops refresh requests while it still sees the content as animating. The single Invalidate at the start of the resize comes before all of that, so no paint at the new size arrived until the 1 s stall kick, sometimes two (about 4 s). cef_host now calls Invalidate every 100 ms while a resize is in flight. device_frame: 0 kicks in 5 runs (before: 1 to 12 per run, and the published c0576c5 build failed one switch after 2 kicks). Slowest switch 1.3 s, down from 1.6 to 3.7 s. Co-Authored-By: Claude Opus 5.5 (1M context) --- example/lib/device_frame_probe.dart | 4 +++ packages/flutter_cef_macos/CHANGELOG.md | 7 ++++++ .../native/cef_host/browser_ops.mm | 25 ++++++++++++++++--- .../native/cef_host/host_state.h | 6 +++-- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/example/lib/device_frame_probe.dart b/example/lib/device_frame_probe.dart index cace074..820b505 100644 --- a/example/lib/device_frame_probe.dart +++ b/example/lib/device_frame_probe.dart @@ -24,6 +24,10 @@ // for JANK_MS per frame around each switch. With FIXED_DPR=true switches change // only the size, which never wedged. // +// cef_host logs `resize: no paint at WxH ... kick N` when a resize waits a +// second without a paint at its size. A healthy run has none; if they're back, +// cef_host has stopped asking for a fresh capture while a resize waits. +// // Run: FLUTTER_CEF_HOST=<.../cef_host.app/Contents/MacOS/cef_host> \ // flutter run -d macos -t lib/device_frame_probe.dart // [--dart-define=ROUNDS=30 --dart-define=LIMIT_MS=4000 diff --git a/packages/flutter_cef_macos/CHANGELOG.md b/packages/flutter_cef_macos/CHANGELOG.md index ab75443..14f40de 100644 --- a/packages/flutter_cef_macos/CHANGELOG.md +++ b/packages/flutter_cef_macos/CHANGELOG.md @@ -1,5 +1,12 @@ ## Unreleased +* Fix: on macOS, a resize could wait a second or more for its first paint at + the new size (up to about 4 s), most often mid-way through an animated device + switch. CEF paints by capturing the view, and after a big resize nothing asked + it to capture again once the renderer's new-size frame was in. `cef_host` now + asks for a fresh capture every 100 ms while a resize waits for its paint. + `device_frame` no longer needs the stall fallback (it did in 1 to 12 of 30 + switches). The prebuilt must be republished. * Fix: a view could stop painting for good after a resize that changed the render scale, typically when switching the device frame in a preview of a page slow to relayout. Hide/show and reload didn't bring it back. `cef_host` now diff --git a/packages/flutter_cef_macos/native/cef_host/browser_ops.mm b/packages/flutter_cef_macos/native/cef_host/browser_ops.mm index 36f235f..73cef18 100644 --- a/packages/flutter_cef_macos/native/cef_host/browser_ops.mm +++ b/packages/flutter_cef_macos/native/cef_host/browser_ops.mm @@ -396,7 +396,16 @@ void DoSetVisible(const std::shared_ptr& slot, bool visible) { // id first. It only gets to do that when no hold is on, so CEF sees geometry // through cef_width/cef_height/cef_dpr, and those change only when no resize is // in flight. Changes the plugin asks for meanwhile collapse into the latest one. +// +// The paint that ends a resize needs a capture after the resize lands. On macOS +// CEF paints by capturing the root frame at the size of the renderer's last +// activated frame. When the renderer's frame at the new size activates, the +// capture size changes, but a page with nothing else changing gives the +// capturer no damage, and it drops refresh requests while it still sees the +// content as animating. The one Invalidate at the start of a resize comes before +// all of that, so ask again every kResizeRefreshMs until the paint arrives. namespace { +constexpr int kResizeRefreshMs = 100; constexpr int kResizePaintWaitMs = 1000; constexpr int kResizePaintMaxWaitMs = 4000; constexpr int kResizeMaxKicks = 6; @@ -437,6 +446,8 @@ void ApplyGeometry(const std::shared_ptr& slot) { host->SendExternalBeginFrame(); slot->resize_in_flight = true; slot->resize_wait_since = std::chrono::steady_clock::now(); + slot->resize_refresh_at = + slot->resize_wait_since + std::chrono::milliseconds(kResizeRefreshMs); slot->resize_wait_ms = kResizePaintWaitMs; slot->resize_kicks = 0; } @@ -455,13 +466,19 @@ void NoteViewPaint(const std::shared_ptr& slot, int pixel_w, int pixel_h) CefPostTask(TID_UI, base::BindOnce(&ApplyGeometry, slot)); } -// The fallback if a resize still doesn't paint. With the hold on and the -// geometry CEF sees unchanged, NotifyScreenInfoChanged only swaps in a fresh -// surface id, which gets the renderer to paint again. Waits back off (1, 2, -// 4 s) so a page that is merely slow to lay out isn't kicked over and over. +// Runs every begin frame while a resize is in flight: asks for a fresh capture +// every kResizeRefreshMs (see above), and kicks if the paint still doesn't come. +// With the hold on and the geometry CEF sees unchanged, NotifyScreenInfoChanged +// only swaps in a fresh surface id, which gets the renderer to paint again. +// Waits back off (1, 2, 4 s) so a page that is merely slow to lay out isn't +// kicked over and over. void CheckResizeStall(const std::shared_ptr& slot) { if (!slot->resize_in_flight || !slot->visible || !slot->browser) return; const auto now = std::chrono::steady_clock::now(); + if (now >= slot->resize_refresh_at) { + slot->browser->GetHost()->Invalidate(PET_VIEW); + slot->resize_refresh_at = now + std::chrono::milliseconds(kResizeRefreshMs); + } if (now - slot->resize_wait_since < std::chrono::milliseconds(slot->resize_wait_ms)) return; diff --git a/packages/flutter_cef_macos/native/cef_host/host_state.h b/packages/flutter_cef_macos/native/cef_host/host_state.h index 55731aa..dcdf02f 100644 --- a/packages/flutter_cef_macos/native/cef_host/host_state.h +++ b/packages/flutter_cef_macos/native/cef_host/host_state.h @@ -209,10 +209,12 @@ struct Slot { // gating. UI-thread only, like `visible`. int pump_interval_ms = 16; // A resize handed to CEF and not yet painted (see ApplyGeometry in - // browser_ops.mm): since when, the pixel size CEF will paint, how long the - // current wait may run before a kick, and the kicks so far. UI-thread only. + // browser_ops.mm): since when, when to next ask for a fresh capture, the + // pixel size CEF will paint, how long the current wait may run before a kick, + // and the kicks so far. UI-thread only. bool resize_in_flight = false; std::chrono::steady_clock::time_point resize_wait_since{}; + std::chrono::steady_clock::time_point resize_refresh_at{}; int resize_want_w = 0; int resize_want_h = 0; int resize_wait_ms = 0;