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;