Skip to content

macOS: a density change during a resize no longer freezes the view - #59

Merged
wenkaifan0720 merged 2 commits into
mainfrom
fix/density-resize-freeze
Sep 29, 2026
Merged

wenkaifan0720 merged 2 commits into
mainfrom
fix/density-resize-freeze

Conversation

@wenkaifan0720

@wenkaifan0720 wenkaifan0720 commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Switching the device frame in FlutterFlow's Test Mode could freeze the preview for good. JS still answered, but nothing painted, and hide/show and reload didn't bring it back. It happens when the previewed page is slow to relayout, like a debug web build.

Cause

A device switch changes the view's size and render scale at once. CEF's OSR view keeps one resize in flight: a size or scale change "holds" until a paint arrives at the new pixel size, and a change made during the hold is queued for when it releases.

A queued density change is applied through RenderWidgetHostViewBase::UpdateScreenInfo. That sends the renderer the new scale under the surface id it already has. The renderer's next frame therefore has a different scale for the same surface id, so viz rejects it as a surface invariants violation and drops the renderer's frame sink.

On macOS the view never comes back from that, for one of two reasons:

  • The renderer returns under a surface id it allocated itself. CEF 144 never embeds that id, because CefRenderWidgetHostViewOSR::DidUpdateVisualProperties is compiled out for Mac (upstream CEF has since removed that guard). The display has no damage it recognises, never draws, and the hold never releases.
  • Or the external begin-frame source waits forever on the dropped sink, so no begin frames go out at all.

Found with --trace-startup: Surface invariants violation → DidLoseLayerTreeFrameSink → ChildLocalSurfaceIdAllocator::GenerateId, followed by no browser-side embed of the new id and no Display::DrawAndSwap for the rest of the run.

This also explains an earlier experiment. Dropping the NotifyScreenInfoChanged call made the freeze worse (round 1 instead of round 2): WasResized alone applies the new density through the same UpdateScreenInfo path.

Fix

  • cef_host now shows CEF its geometry through cef_width / cef_height / cef_dpr, via GetViewRect and GetScreenInfo.
  • Those values change only when no resize is in flight. A resize ends when CEF paints the exact pixel size it will hold for (ScaleToCeiledSize in float), and whatever the plugin asked for meanwhile is applied next. Intermediate sizes collapse into the latest one.
  • A density change goes through NotifyScreenInfoChanged, which retires the surface id before applying the new scale.
  • Fallback: a resize with no paint at its size gets NotifyScreenInfoChanged again, 1, 2, then 4 s apart, up to 6 times. With the geometry CEF sees unchanged, that only swaps in a fresh surface id.
  • Windows is unchanged. It doesn't use external begin frames, and CEF on Windows embeds renderer-allocated ids.

Verification

New probe device_frame, added to run_probes.sh, mirrors Test Mode's widget tree: FittedBox, MediaQuery, CefWebView at devicePixelRatio × zoom. It switches devices 30 times in three ways (plain, parked-then-shown, animated). The page blocks its main thread 600 ms on every resize, and the UI thread is janked 40 ms per frame. Each round checks the page's viewport and density, then that it still presents a frame.

host (shipping framework) result
before froze at round 2, 3 of 3 runs
after 30/30, 3 of 3 runs; no invariants violation in a 40 s trace

Full tool/run_probes.sh: 18 of 19 passed. The one failure, host_robustness, is a pre-existing flake: a sentinel eval misses its 5 s window in a random case. It fails the same way on the baseline host (interleaved A/B: baseline 3/4, this branch 4/4). That probe creates its tiles headless and never resizes them, so none of the new code acts there.

A second commit hands CEF the latest geometry in OnAfterCreated. GetViewRect now reports what CEF was handed, so without it a resize that arrived while the browser was being created would have waited for the next resize.

The prebuilt must be republished (make publish-cef-host) before consumers repin.

🤖 Generated with Claude Code

wenkaifan0720 and others added 2 commits September 29, 2026 12:06
Switching the device frame in FlutterFlow's Test Mode changes the preview's
size and render scale at once. On a page slow to relayout the view could stop
painting for good: JS still answered, hide/show and reload didn't help.

CEF's OSR view keeps one resize in flight and queues a change made meanwhile.
A queued density change is applied through UpdateScreenInfo, which sends the
renderer the new scale under the surface id it already has. viz rejects the
renderer's next frame (surface invariants violation) and drops its frame sink.
On macOS the view then never draws again: CEF 144 never embeds the surface id
the renderer allocates for its new sink (DidUpdateVisualProperties is compiled
out for Mac), or the external begin-frame source waits forever on the dropped
sink.

cef_host now hands CEF geometry through cef_width/cef_height/cef_dpr and
changes them only when no resize is in flight (a paint at the new pixel size
ends one). A density change goes through NotifyScreenInfoChanged, which
retires the surface id first. A resize with no paint at its size gets the
renderer a fresh surface id, 1, 2, then 4 s apart.

Probe: device_frame (slow page, busy UI thread, plain/blip/anim switches).
Before: froze at round 2 in 3 of 3 runs on the shipping framework. After:
30/30 in 3 of 3 runs, no invariants violation in a 40 s trace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CEF builds its view at the create size, and GetViewRect now reports the
geometry handed to CEF rather than the latest request, so a resize that
landed before OnAfterCreated would wait for the next one. Hand CEF the
latest geometry when the browser binds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@wenkaifan0720
wenkaifan0720 merged commit c0576c5 into main Sep 29, 2026
2 of 3 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.

1 participant