diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 5c40368a2..163b2ab1b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -340,7 +340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: b47e8385e5a75d38319ad706a64b0ead3108b093 + resolved-ref: f8c4fce53014e21b9b58e9f12382bcd45d984d5d url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp index 903fb2fa0..501436d71 100644 --- a/flutter/windows/runner/flutter_window.cpp +++ b/flutter/windows/runner/flutter_window.cpp @@ -19,6 +19,66 @@ #include "win32_desktop.h" +namespace { + +// If the window is resized between the creation of the Flutter surface and the +// present of the first frame - which is what the PowerToys FancyZones option +// "Move newly created windows to their last known zone" does - the embedder's +// resize synchronization enters kResizeStarted and from then on only presents +// frames that match the new size. A frame already generated for the old size +// is rejected, nothing schedules a matching one, and the window stays white +// until a real resize re-enters OnWindowSizeChanged, which resets the resize +// target and resends the window metrics. That is why minimize/restore heals +// it; ForceChildRefresh() below does the same programmatically. +// https://github.com/rustdesk/rustdesk/issues/6756 +// https://github.com/flutter/flutter/issues/159630 +// +// The timer below drives that recovery. Two subtleties, verified against the +// embedder sources (identical in 3.24.5 and 3.44.0): +// - FlutterViewController::ForceRedraw() only schedules a frame when NO resize +// is pending (resize_status_ == kDone), so it cannot heal the wedge above. +// It is kept as a cheap first kick for the case it was designed for: a +// window created hidden and shown later, with nothing scheduling a frame. +// - The SetNextFrameCallback used to detect the first frame fires when a frame +// is GENERATED (raster thread), even if the resize gate then rejects its +// present. So it must not be the only stop condition: one final +// ForceChildRefresh() is issued to guarantee a present at the current size. +// Note this premise is not load-bearing, and the redundancy is deliberate: +// if the callback in fact only fired on a successful present, then +// first_frame_rendered_ would stay false and the timer below would keep +// nudging until it healed. +// This also relies on HandleTopLevelWindowProc not consuming WM_TIMER (no +// plugin registers a delegate for it today). +constexpr UINT_PTR kForceRedrawTimerId = 0xFB15; +constexpr UINT kForceRedrawIntervalMs = 200; +// Give up eventually (with a log), so a genuinely stuck engine doesn't keep a +// timer alive forever. 25 * 200ms covers slow starts comfortably. +constexpr UINT kForceRedrawMaxTries = 25; +// The first ticks use the cheap ForceRedraw(); later ticks use +// ForceChildRefresh(), which may block the platform thread for up to 2x100ms +// per call (each nudge re-enters the 100ms resize wait). +constexpr UINT kForceRedrawCheapTries = 2; + +// Re-enters the embedder's OnWindowSizeChanged by nudging the Flutter child +// window by 1px and back: this resets the resize target and resends the window +// metrics. Same as BaseFlutterWindow::ForceChildRefresh() on the +// rustdesk_desktop_multi_window side. +void ForceChildRefresh(HWND child) { + if (!child) { + return; + } + RECT rect; + GetWindowRect(child, &rect); + LONG width = rect.right - rect.left; + LONG height = rect.bottom - rect.top; + SetWindowPos(child, nullptr, 0, 0, width + 1, height, + SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOMOVE | SWP_FRAMECHANGED); + SetWindowPos(child, nullptr, 0, 0, width, height, + SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOMOVE | SWP_FRAMECHANGED); +} + +} // namespace + FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -92,10 +152,17 @@ bool FlutterWindow::OnCreate() { registry->GetRegistrarForPlugin("FlutterGpuTextureRendererPluginCApi")); }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + // See the comment on kForceRedrawTimerId above. + flutter_controller_->engine()->SetNextFrameCallback( + [this]() { first_frame_rendered_ = true; }); + SetTimer(GetHandle(), kForceRedrawTimerId, kForceRedrawIntervalMs, nullptr); + return true; } void FlutterWindow::OnDestroy() { + KillTimer(GetHandle(), kForceRedrawTimerId); if (flutter_controller_) { flutter_controller_ = nullptr; } @@ -121,6 +188,48 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message, case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; + case WM_TIMER: + if (wparam == kForceRedrawTimerId) { + if (!flutter_controller_) { + KillTimer(hwnd, kForceRedrawTimerId); + } else if (first_frame_rendered_) { + // A frame was generated, which does not mean it was presented: if a + // resize was pending, the gate rejected it (see the comment on + // kForceRedrawTimerId). One child refresh guarantees a present at the + // current size. Unconditional because gating it bought nothing: the + // WM_SIZE that CreateWindow() sends already arrives before the first + // frame, so the flag this used to check was always set by the time we + // got here. Doing it unconditionally is safe either way - at worst it + // is one extra nudge, and it is cheap once the engine is running. + ForceChildRefresh(flutter_controller_->view()->GetNativeWindow()); + KillTimer(hwnd, kForceRedrawTimerId); + } else if (++force_redraw_tries_ > kForceRedrawMaxTries) { + // Not std::cerr: the runner only attaches a console when started from + // one or under a debugger (see main.cpp), and this fires on end-user + // machines. OutputDebugString is readable with DebugView there. + OutputDebugStringA( + "rustdesk: Flutter window did not render its first frame, " + "giving up.\n"); + KillTimer(hwnd, kForceRedrawTimerId); + } else if (force_redraw_tries_ <= kForceRedrawCheapTries) { + flutter_controller_->ForceRedraw(); + } else { + ForceChildRefresh(flutter_controller_->view()->GetNativeWindow()); + } + return 0; + } + break; + case WM_SHOWWINDOW: + // A window created hidden (e.g. the connection manager) may be shown + // long after the creation-time force-redraw timer has given up, and + // FancyZones moves windows exactly when they are shown. Re-arm the + // protection if the first frame still hasn't been rendered by now (see + // kForceRedrawTimerId). + if (wparam == TRUE && !first_frame_rendered_ && flutter_controller_) { + force_redraw_tries_ = 0; + SetTimer(hwnd, kForceRedrawTimerId, kForceRedrawIntervalMs, nullptr); + } + break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); diff --git a/flutter/windows/runner/flutter_window.h b/flutter/windows/runner/flutter_window.h index 6da0652f0..e7aad49e2 100644 --- a/flutter/windows/runner/flutter_window.h +++ b/flutter/windows/runner/flutter_window.h @@ -28,6 +28,14 @@ class FlutterWindow : public Win32Window { // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; + + // Whether the engine has generated its first frame. Note that a generated + // frame is not necessarily presented: the resize synchronization may reject + // it (see kForceRedrawTimerId in the .cpp file). + bool first_frame_rendered_ = false; + + // Number of force-redraw attempts made so far. + UINT force_redraw_tries_ = 0; }; #endif // RUNNER_FLUTTER_WINDOW_H_