Skip to content

Animation stutters on Windows desktop, but is smooth on Chrome or MacOS desktop #97334

Open

Description

@caudetgit

Issue
With the Rive Flutter application, playback with complex animations is smooth on Windows Chrome and MacOS desktop, but noticeable stuttering occurs on Windows desktop. Same build, same project file - the only difference is the build target. I believe Flutter was using Angle to convert OpenGL to DirectX on the fly, which is not as performant as Skia's native DirectX backend. Not sure if this is still the case and/or relevant.

We've reproduced this behavior across a number of Windows desktops, including machines with RTX 3080 graphics cards.

Target Platform: Windows desktop 10+
Devices: windows-x64 (e.g., Microsoft Windows Version 10.0.19044.1466)
Flutter version: Repro'd with 2.8, 2.9 and 2.10.0-0.2.pre

Windows desktop video Pay attention to the rocks, tree and castle on the rotating planet below the knight. As these elements rotate you can see a significant stutter or "hitch" every few frames. You can also see this behavior with the blue vertical playhead line in the animation timeline.

[Note: The video capture software introduced a teensy bit of playback artifacts, but these videos are 99% accurate to the actual application experience.]

Rive_WinDesktop_stuttering.mp4
Windows Chrome video Same computer, version of Rive, account and project file as above, but running in Chrome. Playback is much smoother than the Win desktop.
Rive_Chrome_no_stuttering.mp4
Logs

flutter analyze

info - The value of the local variable 'cubic' isn't used - lib\rive\stage\items\stage_path_vertex.dart:49:11 - unused_local_variable
info - This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: RiveTreeView.scrollController, RiveTreeView.physics - lib\widgets\tree_view\rive_tree_view.dart:5:7 - must_be_immutable
info - The value of the local variable 'tasksApi' isn't used - test\managers\push_event_manager_test.dart:47:23 - unused_local_variable

flutter doctor -v

[√] Flutter (Channel unknown, 2.10.0-0.2.pre, on Microsoft Windows [Version 10.0.19044.1466], locale en-US)
• Flutter version 2.10.0-0.2.pre at C:\Users\chada\Development\flutter
• Upstream repository unknown
• Framework revision 73adb14 (7 days ago), 2022-01-19 17:02:56 -0600
• Engine revision 74b74b1
• Dart version 2.16.0 (build 2.16.0-134.1.beta)
• DevTools version 2.9.2

[!] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
• Android SDK at C:\Users\chada\AppData\Local\Android\sdk
X cmdline-tools component is missing
Run path/to/sdkmanager --install "cmdline-tools;latest"
See https://developer.android.com/studio/command-line for more details.
X Android license status unknown.
Run flutter doctor --android-licenses to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.

[√] Chrome - develop for the web
• Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.0.4)
• Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
• Visual Studio Community 2022 version 17.0.32014.148
• Windows 10 SDK version 10.0.22000.0

[√] Android Studio (version 2020.3)
• Android Studio at C:\Program Files\Android\Android Studio
• Flutter plugin can be installed from:
https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)

[√] VS Code (version 1.63.2)
• VS Code at C:\Users\chada\AppData\Local\Programs\Microsoft VS Code
• Flutter extension version 3.32.0

[√] Connected device (3 available)
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.19044.1466]
• Chrome (web) • chrome • web-javascript • Google Chrome 97.0.4692.99
• Edge (web) • edge • web-javascript • Microsoft Edge 97.0.1072.55

[√] HTTP Host Availability
• All required HTTP hosts are available

! Doctor found issues in 1 category.

Activity

  1. caudetgit commented on Jan 27, 2022

    @caudetgit
    Author

    We created a small project using the public flutter app with a sample animation showing the issue (same animation as in the videos above).
    https://github.com/luigi-rosso/flutter_performance_97334

  2. csells commented on Jan 27, 2022

    @csells
    Contributor

    Thanks, @caudetgit . We'll take a look.

    cc @HansMuller

  3. added
    in triagePresently being triaged by the triage team
    and removed on Jan 28, 2022
  4. maheshj01 commented on Jan 28, 2022

    @maheshj01
    Member

    Hi @caudetgit, Thanks for filing the issue. I can barely see a difference on windows/ macOS Desktop. I have attached the timeline-trace from the master channel. The app was run in profile mode with --trace-skia on both the platforms.

    Windows desktop

    flutter_performance_97334.2022-01-28.19-50-39_Trim.mp4

    MacOS desktop

    Screen.Recording.2022-01-28.at.8.08.07.PM.mov
    flutter doctor -v (windows)
    [√] Flutter (Channel master, 2.10.0-1.0.pre.317, on Microsoft Windows [Version 10.0.19042.1466], locale
        en-US)
        • Flutter version 2.10.0-1.0.pre.317 at C:\flutter_sdk\master
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 4621dff892 (8 hours ago), 2022-01-28 11:50:13 +0530
        • Engine revision 5fa79de88d
        • Dart version 2.17.0 (build 2.17.0-66.0.dev)
        • DevTools version 2.10.0-dev.1
    [√] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
        • Android SDK at C:\Users\mahesh\AppData\Local\Android\sdk
        • Platform android-31, build-tools 31.0.0
        • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
        • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)
        • All Android licenses accepted.
    [√] Chrome - develop for the web
        • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
    [√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.11.4)
        • Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
        • Visual Studio Community 2019 version 16.11.31727.386
        • Windows 10 SDK version 10.0.19041.0
    [√] Android Studio (version 2020.3)
        • Android Studio at C:\Program Files\Android\Android Studio
        • Flutter plugin can be installed from:
           https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
           https://plugins.jetbrains.com/plugin/6351-dart
        • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)
    [√] VS Code (version 1.63.2)
        • VS Code at C:\Users\mahesh\AppData\Local\Programs\Microsoft VS Code
        • Flutter extension version 3.32.0
    [√] Connected device (4 available)
        • Windows (desktop)       • windows • windows-x64     • Microsoft Windows [Version 10.0.19042.1466]
        • Windows (UWP) (desktop) • winuwp  • windows-uwp-x64 •
        • Chrome (web)            • chrome  • web-javascript  • Google Chrome 97.0.4692.99
        • Edge (web)              • edge    • web-javascript  • Microsoft Edge 97.0.1072.69
    [√] HTTP Host Availability
        • All required HTTP hosts are available
    • No issues found!
    
    flutter doctor -v (macos)
    [✓] Flutter (Channel master, 2.10.0-1.0.pre.319, on macOS 12.1 21C52 darwin-arm, locale en-GB)
        • Flutter version 2.10.0-1.0.pre.319 at /Users/mahesh/Documents/flutter_master
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 120d25f786 (4 hours ago), 2022-01-28 12:40:06 +0200
        • Engine revision c5ee2e4c35
        • Dart version 2.17.0 (build 2.17.0-66.0.dev)
        • DevTools version 2.10.0-dev.1
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
        • Android SDK at /Users/mahesh/Library/Android/sdk
        • Platform android-31, build-tools 31.0.0
        • ANDROID_HOME = /Users/mahesh/Library/Android/sdk
        • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
        • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • CocoaPods version 1.10.2
    
    [✓] Chrome - develop for the web
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Android Studio (version 4.2)
        • Android Studio at /Applications/Android Studio.app/Contents
        • Flutter plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/9212-flutter
        • Dart plugin can be installed from:
          🔨 https://plugins.jetbrains.com/plugin/6351-dart
        • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    
    [✓] IntelliJ IDEA Community Edition (version 2021.2.1)
        • IntelliJ at /Applications/IntelliJ IDEA CE.app
        • Flutter plugin version 60.1.4
        • Dart plugin version 212.5080.8
    
    [✓] VS Code (version 1.63.2)
        • VS Code at /Applications/Visual Studio Code.app/Contents
        • Flutter extension version 3.32.0
    
    [✓] Connected device (3 available)
        • Redmi K20 Pro (mobile) • 192.168.1.2:5555 • android-arm64  • Android 11 (API 30)
        • macOS (desktop)        • macos            • darwin-arm64   • macOS 12.1 21C52 darwin-arm
        • Chrome (web)           • chrome           • web-javascript • Google Chrome 97.0.4692.99
    
    [✓] HTTP Host Availability
        • All required HTTP hosts are available
    
    • No issues found!
    

    I could not verify it on the web the animation failed to load with XMLHttpRequest

    flutter run -d chrome
    mahesh@Maheshs-MacBook-Air-M1 flutter_performance_97334 % flutter run -d chrome
    Launching lib/main.dart on Chrome in debug mode...
    Waiting for connection from debug service on Chrome...             30.8s
    This app is linked to the debug service: ws://127.0.0.1:63529/LrkOPL_9EbI=/ws
    Debug service listening on ws://127.0.0.1:63529/LrkOPL_9EbI=/ws
    
    💪 Running with sound null safety 💪
    
    🔥  To hot restart changes while running, press "r" or "R".
    For a more detailed help message, press "h". To quit, press "q".
    
    An Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:63529/LrkOPL_9EbI=
    Error: XMLHttpRequest error.
        dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 926:28                get current
    packages/http/src/browser_client.dart 69:22                                       <fn>
    dart-sdk/lib/async/zone.dart 1685:54                                              runUnary
    dart-sdk/lib/async/future_impl.dart 159:18                                        handleValue
    dart-sdk/lib/async/future_impl.dart 766:44                                        handleValueCallback
    dart-sdk/lib/async/future_impl.dart 795:13                                        _propagateToListeners
    dart-sdk/lib/async/future_impl.dart 592:7                                         [_complete]
    dart-sdk/lib/async/stream_pipe.dart 61:11                                         _cancelAndValue
    dart-sdk/lib/async/stream.dart 1530:7                                             <fn>
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 334:14  _checkAndCall
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 339:39  dcall
    dart-sdk/lib/html/dart2js/html_dart2js.dart 37321:58                              <fn>
    
    
        at Object.createErrorWithStack (http://localhost:63488/dart_sdk.js:5082:12)
        at Function._throw (http://localhost:63488/dart_sdk.js:20329:18)
        at Function.throwWithStackTrace (http://localhost:63488/dart_sdk.js:20326:18)
        at async._AsyncCallbackEntry.new.callback (http://localhost:63488/dart_sdk.js:40852:18)
        at Object._microtaskLoop (http://localhost:63488/dart_sdk.js:40709:13)
        at _startMicrotaskLoop (http://localhost:63488/dart_sdk.js:40715:13)
        at http://localhost:63488/dart_sdk.js:36192:9
    The Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:63529/LrkOPL_9EbI=
    

    Labeling this issue for further investigation.

  5. added
    platform-windowsBuilding on or for Windows specifically
    c: performanceRelates to speed or footprint issues (see "perf:" labels)
    frameworkflutter/packages/flutter repository. See also f: labels.
    perf: speedPerformance issues related to (mostly rendering) speed
    and removed
    in triagePresently being triaged by the triage team
    on Jan 28, 2022
  6. gspencergoog commented on Feb 3, 2022

    @gspencergoog
    Contributor

    cc @iskakaushik Is this something you have an interest in?

  7. 63 remaining items

  8. withliyh commented on Jan 30, 2024

    @withliyh

    @kobutri Could you try this patch? This uses higher precision frame timing info and uses the frame timing info to determine the frame's start time.

    diff --git a/shell/platform/windows/angle_surface_manager.cc b/shell/platform/windows/angle_surface_manager.cc
    index 18d21f6b42..721ccd7af3 100644
    --- a/shell/platform/windows/angle_surface_manager.cc
    +++ b/shell/platform/windows/angle_surface_manager.cc
    @@ -305,6 +305,28 @@ EGLSurface AngleSurfaceManager::CreateSurfaceFromHandle(
                                               egl_config_, attributes);
     }
     
    +void* AngleSurfaceManager::QueryDeviceObject(int object_type) {
    +  intptr_t egl_device = 0;
    +  if (!eglQueryDisplayAttribEXT(egl_display_, EGL_DEVICE_EXT, &egl_device)) {
    +    FML_LOG(ERROR) << "eglQueryDisplayAttribEXT failed";
    +    return nullptr;
    +  }
    +
    +  if (!egl_device) {
    +    FML_LOG(ERROR) << "Failed to retrieve EGLDeviceEXT";
    +    return nullptr;
    +  }
    +
    +  intptr_t device = 0;
    +  if (!eglQueryDeviceAttribEXT(reinterpret_cast<EGLDeviceEXT>(egl_device),
    +                               object_type, &device)) {
    +    FML_LOG(ERROR) << "eglQueryDeviceAttribEXT failed";
    +    return nullptr;
    +  }
    +
    +  return reinterpret_cast<void*>(device);
    +}
    +
     void AngleSurfaceManager::SetVSyncEnabled(bool enabled) {
       if (eglMakeCurrent(egl_display_, render_surface_, render_surface_,
                          egl_context_) != EGL_TRUE) {
    diff --git a/shell/platform/windows/angle_surface_manager.h b/shell/platform/windows/angle_surface_manager.h
    index edfe7a8cbc..71dc9b8f92 100644
    --- a/shell/platform/windows/angle_surface_manager.h
    +++ b/shell/platform/windows/angle_surface_manager.h
    @@ -75,6 +75,8 @@ class AngleSurfaceManager {
                                          EGLClientBuffer handle,
                                          const EGLint* attributes) const;
     
    +  void* QueryDeviceObject(int object_type);
    +
       // Gets the |EGLDisplay|.
       EGLDisplay egl_display() const { return egl_display_; };
     
    diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc
    index 39dbe8ea6b..187c8c648d 100644
    --- a/shell/platform/windows/flutter_windows_engine.cc
    +++ b/shell/platform/windows/flutter_windows_engine.cc
    @@ -4,7 +4,10 @@
     
     #include "flutter/shell/platform/windows/flutter_windows_engine.h"
     
    +#include <d3d11.h>
     #include <dwmapi.h>
    +#include <windows.h>
    +#include <wrl/client.h>
     
     #include <filesystem>
     #include <sstream>
    @@ -25,21 +28,47 @@
     // winbase.h defines GetCurrentTime as a macro.
     #undef GetCurrentTime
     
    +static constexpr double kMicrosecondsPerSecond = 1000000.0;
    +static constexpr int64_t kQPCOverflowThreshold = INT64_C(0x8637BD05AF7);
    +
     static constexpr char kAccessibilityChannelName[] = "flutter/accessibility";
     
     namespace flutter {
     
     namespace {
     
    -// Lifted from vsync_waiter_fallback.cc
    -static std::chrono::nanoseconds SnapToNextTick(
    -    std::chrono::nanoseconds value,
    -    std::chrono::nanoseconds tick_phase,
    -    std::chrono::nanoseconds tick_interval) {
    -  std::chrono::nanoseconds offset = (tick_phase - value) % tick_interval;
    -  if (offset != std::chrono::nanoseconds::zero())
    -    offset = offset + tick_interval;
    -  return value + offset;
    +Microsoft::WRL::ComPtr<IDXGIOutput> DXGIOutputFromMonitor(
    +    HMONITOR monitor,
    +    const Microsoft::WRL::ComPtr<ID3D11Device>& d3d11_device) {
    +  Microsoft::WRL::ComPtr<IDXGIDevice> dxgi_device;
    +  if (FAILED(d3d11_device.As(&dxgi_device))) {
    +    FML_LOG(ERROR) << "Failed to retrieve DXGI device";
    +    return nullptr;
    +  }
    +
    +  Microsoft::WRL::ComPtr<IDXGIAdapter> dxgi_adapter;
    +  if (FAILED(dxgi_device->GetAdapter(&dxgi_adapter))) {
    +    FML_LOG(ERROR) << "Failed to retrieve DXGI adapter";
    +    return nullptr;
    +  }
    +
    +  size_t i = 0;
    +  while (true) {
    +    Microsoft::WRL::ComPtr<IDXGIOutput> output;
    +    if (FAILED(dxgi_adapter->EnumOutputs(i++, &output)))
    +      break;
    +
    +    DXGI_OUTPUT_DESC desc = {};
    +    if (FAILED(output->GetDesc(&desc))) {
    +      FML_LOG(ERROR) << "DXGI output GetDesc failed";
    +      return nullptr;
    +    }
    +
    +    if (desc.Monitor == monitor)
    +      return output;
    +  }
    +
    +  return nullptr;
     }
     
     // Creates and returns a FlutterRendererConfig that renders to the view (if any)
    @@ -331,10 +360,64 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
         auto host = static_cast<FlutterWindowsEngine*>(user_data);
         return host->HandlePlatformMessage(engine_message);
       };
    +  vsync_batons_ = {};
       args.vsync_callback = [](void* user_data, intptr_t baton) -> void {
         auto host = static_cast<FlutterWindowsEngine*>(user_data);
    -    host->OnVsync(baton);
    +    {
    +      std::scoped_lock lock{host->vsync_mutex_};
    +      host->vsync_batons_.push_back(baton);
    +    }
    +    host->vsync_latch_.Signal();
       };
    +
    +  std::thread([this]() {
    +    ::SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
    +
    +    LARGE_INTEGER ticks_per_sec = {};
    +    if (::QueryPerformanceFrequency(&ticks_per_sec)) {
    +      qpc_ticks_per_second_ = ticks_per_sec.QuadPart;
    +    }
    +
    +    Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device =
    +        reinterpret_cast<ID3D11Device*>(
    +            surface_manager_->QueryDeviceObject(EGL_D3D11_DEVICE_ANGLE));
    +
    +    while (true) {
    +      vsync_latch_.Wait();
    +
    +      const HMONITOR monitor =
    +          ::MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
    +      if (primary_monitor_ != monitor) {
    +        primary_monitor_ = monitor;
    +        primary_output_ = DXGIOutputFromMonitor(monitor, d3d11_device);
    +      }
    +
    +      std::chrono::nanoseconds frame_start_time, interval;
    +      std::tie(frame_start_time, interval) = GetVsyncTimingInfo();
    +
    +      primary_output_->WaitForVBlank();
    +
    +      std::vector<intptr_t> vsync_batons;
    +      {
    +        std::scoped_lock lock{vsync_mutex_};
    +        vsync_batons = std::move(vsync_batons_);
    +        vsync_batons_.clear();
    +      }
    +
    +      if (frame_start_time.count() == 0) {
    +        frame_start_time =
    +            std::chrono::nanoseconds(embedder_api_.GetCurrentTime());
    +      }
    +
    +      std::chrono::nanoseconds frame_target_time = frame_start_time + interval;
    +
    +      for (intptr_t baton : vsync_batons) {
    +        embedder_api_.OnVsync(engine_, baton, frame_start_time.count(),
    +                              frame_target_time.count());
    +      }
    +    }
    +  }).detach();
    +
       args.on_pre_engine_restart_callback = [](void* user_data) {
         auto host = static_cast<FlutterWindowsEngine*>(user_data);
         host->OnPreEngineRestart();
    @@ -389,7 +472,8 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
       display.display_id = 0;
       display.single_display = true;
       display.refresh_rate =
    -      1.0 / (static_cast<double>(FrameInterval().count()) / 1000000000.0);
    +      1.0 / (static_cast<double>(GetVsyncTimingInfo().second.count()) /
    +             kMicrosecondsPerSecond);
     
       std::vector<FlutterEngineDisplay> displays = {display};
       embedder_api_.NotifyDisplayUpdate(engine_,
    @@ -422,32 +506,80 @@ void FlutterWindowsEngine::SetView(FlutterWindowsView* view) {
       InitializeKeyboard();
     }
     
    -void FlutterWindowsEngine::OnVsync(intptr_t baton) {
    -  std::chrono::nanoseconds current_time =
    -      std::chrono::nanoseconds(embedder_api_.GetCurrentTime());
    -  std::chrono::nanoseconds frame_interval = FrameInterval();
    -  auto next = SnapToNextTick(current_time, start_time_, frame_interval);
    -  embedder_api_.OnVsync(engine_, baton, next.count(),
    -                        (next + frame_interval).count());
    -}
    +void FlutterWindowsEngine::OnVsync(intptr_t baton) {}
     
    -std::chrono::nanoseconds FlutterWindowsEngine::FrameInterval() {
    -  if (frame_interval_override_.has_value()) {
    -    return frame_interval_override_.value();
    -  }
    -  uint64_t interval = 16600000;
    +std::pair<std::chrono::nanoseconds, std::chrono::nanoseconds>
    +FlutterWindowsEngine::GetVsyncTimingInfo() {
    +  // if (frame_interval_override_.has_value()) {
    +  //   return frame_interval_override_.value();
    +  // }
    +
    +  uint64_t timebase = 0;
    +  uint64_t interval = 16600;
     
       DWM_TIMING_INFO timing_info = {};
       timing_info.cbSize = sizeof(timing_info);
       HRESULT result = DwmGetCompositionTimingInfo(NULL, &timing_info);
    -  if (result == S_OK && timing_info.rateRefresh.uiDenominator > 0 &&
    -      timing_info.rateRefresh.uiNumerator > 0) {
    -    interval = static_cast<double>(timing_info.rateRefresh.uiDenominator *
    -                                   1000000000.0) /
    -               static_cast<double>(timing_info.rateRefresh.uiNumerator);
    +  if (result == S_OK) {
    +    uint64_t rate_interval;
    +    if (timing_info.rateRefresh.uiDenominator > 0 &&
    +        timing_info.rateRefresh.uiNumerator > 0) {
    +      rate_interval = timing_info.rateRefresh.uiDenominator *
    +                      kMicrosecondsPerSecond /
    +                      timing_info.rateRefresh.uiNumerator;
    +    }
    +
    +    bool is_high_resolution = qpc_ticks_per_second_ > 0;
    +    if (is_high_resolution) {
    +      timebase =
    +          QPCValueToMicroseconds(static_cast<LONGLONG>(timing_info.qpcVBlank));
    +
    +      interval = QPCValueToMicroseconds(
    +          static_cast<LONGLONG>(timing_info.qpcRefreshPeriod));
    +
    +      // Check for interval values that are impossibly low. A 29 microsecond
    +      // interval was seen (from a qpcRefreshPeriod of 60).
    +      if (interval < 1000 /*base::Milliseconds(1)*/) {
    +        interval = rate_interval;
    +      }
    +      // Check for the qpcRefreshPeriod interval being improbably small
    +      // compared to the rateRefresh calculated interval, as another
    +      // attempt at detecting driver bugs.
    +      if (rate_interval > 0 && interval < rate_interval / 2) {
    +        interval = rate_interval;
    +      }
    +    } else {
    +      // If FrameTime is not high resolution, we do not want to translate
    +      // the QPC value provided by DWM into the low-resolution timebase,
    +      // which would be error prone and jittery. As a fallback, we assume
    +      // the timebase is zero and use rateRefresh, which may be rounded but
    +      // isn't noisy like qpcRefreshPeriod, instead. The fact that we don't
    +      // have a timebase here may lead to brief periods of jank when our
    +      // scheduling becomes offset from the hardware vsync.
    +      interval = rate_interval;
    +    }
       }
     
    -  return std::chrono::nanoseconds(interval);
    +  return std::make_pair(std::chrono::duration_cast<std::chrono::nanoseconds>(
    +                            std::chrono::microseconds(timebase)),
    +                        std::chrono::duration_cast<std::chrono::nanoseconds>(
    +                            std::chrono::microseconds(interval)));
    +}
    +
    +int64_t FlutterWindowsEngine::QPCValueToMicroseconds(LONGLONG qpc_value) {
    +  BASE_DCHECK(qpc_ticks_per_second_ > 0);
    +
    +  if (qpc_value < kQPCOverflowThreshold) {
    +    return qpc_value * kMicrosecondsPerSecond / qpc_ticks_per_second_;
    +  } else {
    +    // Otherwise, calculate microseconds in a round about manner to avoid
    +    // overflow and precision issues.
    +    int64_t whole_seconds = qpc_value / qpc_ticks_per_second_;
    +    int64_t leftover_ticks =
    +        qpc_value - (whole_seconds * qpc_ticks_per_second_);
    +    return (whole_seconds * kMicrosecondsPerSecond) +
    +           ((leftover_ticks * kMicrosecondsPerSecond) / qpc_ticks_per_second_);
    +  }
     }
     
     // Returns the currently configured Plugin Registrar.
    diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h
    index 797cc2142d..ea250e5ada 100644
    --- a/shell/platform/windows/flutter_windows_engine.h
    +++ b/shell/platform/windows/flutter_windows_engine.h
    @@ -15,6 +15,7 @@
     
     #include "flutter/fml/closure.h"
     #include "flutter/fml/macros.h"
    +#include "flutter/fml/synchronization/waitable_event.h"
     #include "flutter/shell/platform/common/accessibility_bridge.h"
     #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h"
     #include "flutter/shell/platform/common/client_wrapper/include/flutter/basic_message_channel.h"
    @@ -375,7 +376,10 @@ class FlutterWindowsEngine {
           plugin_registrar_destruction_callbacks_;
     
       // The approximate time between vblank events.
    -  std::chrono::nanoseconds FrameInterval();
    +  std::pair<std::chrono::nanoseconds, std::chrono::nanoseconds>
    +  GetVsyncTimingInfo();
    +
    +  int64_t QPCValueToMicroseconds(LONGLONG qpc_value);
     
       // The start time used to align frames.
       std::chrono::nanoseconds start_time_ = std::chrono::nanoseconds::zero();
    @@ -404,6 +408,13 @@ class FlutterWindowsEngine {
       std::unique_ptr<WindowsLifecycleManager> lifecycle_manager_;
     
       FML_DISALLOW_COPY_AND_ASSIGN(FlutterWindowsEngine);
    +
    +  int64_t qpc_ticks_per_second_ = 0;
    +  Microsoft::WRL::ComPtr<IDXGIOutput> primary_output_;
    +  HMONITOR primary_monitor_ = NULL;
    +  std::mutex vsync_mutex_;
    +  fml::AutoResetWaitableEvent vsync_latch_;
    +  std::vector<intptr_t> vsync_batons_;
     };
     
     }  // namespace flutter

    Please make sure to do a profile or release build. Let me know if you run into any issues!

    Based on this patch, I made a few modifications, and there is almost no jitter or frame dropping on Windows 10 and Windows 11 systems.

    1. Enabled EGL_DIRECT_COMPOSITION_ANGLE.
    2. Changed frame_start_time to the current time when vsync is triggered.
  9. kobutri commented on Jan 30, 2024

    @kobutri

    @withliyh Thank you so much for taking up this issue. I'm having trouble applying your patch. Apparently the angle surface manager stuff was moved in #49900. Could you tell me on which commit of your repo you created the patch or alternatively recreate it on the latest version?

  10. withliyh commented on Jan 31, 2024

    @withliyh

    @kobutri on the 3.16.8 branch, commit: 6e2ea58a5ccbefcfadc2d4b7b8045fc6c20ab812

  11. withliyh commented on Jan 31, 2024

    @withliyh

    @loic-sharma @kobutri full changes, Hope this is helpful for diagnosing the issue.

    diff --git a/shell/platform/windows/angle_surface_manager.cc b/shell/platform/windows/angle_surface_manager.cc
    index 7005348482..7692e7498e 100644
    --- a/shell/platform/windows/angle_surface_manager.cc
    +++ b/shell/platform/windows/angle_surface_manager.cc
    @@ -241,7 +241,11 @@ bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target,
     
       const EGLint surfaceAttributes[] = {
           EGL_FIXED_SIZE_ANGLE, EGL_TRUE, EGL_WIDTH, width,
    -      EGL_HEIGHT,           height,   EGL_NONE};
    +                                      EGL_HEIGHT,
    +                                      height,
    +                                      EGL_DIRECT_COMPOSITION_ANGLE,
    +                                      EGL_TRUE,
    +                                      EGL_NONE};
     
       surface = eglCreateWindowSurface(
           egl_display_, egl_config_,
    @@ -341,6 +345,27 @@ EGLSurface AngleSurfaceManager::CreateSurfaceFromHandle(
                                               egl_config_, attributes);
     }
     
    + void* AngleSurfaceManager::QueryDeviceObject(int object_type) {
    +  intptr_t egl_device = 0;
    +  if (!eglQueryDisplayAttribEXT(egl_display_, EGL_DEVICE_EXT, &egl_device)) {
    +    FML_LOG(ERROR) << "eglQueryDisplayAttribEXT failed";
    +    return nullptr;
    +  }
    +  if (!egl_device) {
    +    FML_LOG(ERROR) << "Failed to retrieve EGLDeviceEXT";
    +    return nullptr;
    +  }
    +  intptr_t device = 0;
    +  if (!eglQueryDeviceAttribEXT(reinterpret_cast<EGLDeviceEXT>(egl_device),
    +                                +object_type, &device)) {
    +    FML_LOG(ERROR) << "eglQueryDeviceAttribEXT failed";
    +    return nullptr;
    +    
    +  }
    +  return reinterpret_cast<void*>(device);
    +  
    +}
    +
     void AngleSurfaceManager::SetVSyncEnabled(bool enabled) {
       // OpenGL swap intervals can be used to prevent screen tearing.
       // If enabled, the raster thread blocks until the v-blank.
    diff --git a/shell/platform/windows/angle_surface_manager.h b/shell/platform/windows/angle_surface_manager.h
    index 2f6302b965..91c1b3493a 100644
    --- a/shell/platform/windows/angle_surface_manager.h
    +++ b/shell/platform/windows/angle_surface_manager.h
    @@ -86,6 +86,8 @@ class AngleSurfaceManager {
                                          EGLClientBuffer handle,
                                          const EGLint* attributes) const;
     
    +  void* QueryDeviceObject(int object_type);
    +
       // Gets the |EGLDisplay|.
       EGLDisplay egl_display() const { return egl_display_; };
     
    diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc
    index 2e04c14f24..e417190c58 100644
    --- a/shell/platform/windows/flutter_windows_engine.cc
    +++ b/shell/platform/windows/flutter_windows_engine.cc
    @@ -3,14 +3,18 @@
     // found in the LICENSE file.
     
     #include "flutter/shell/platform/windows/flutter_windows_engine.h"
    -
    +#include <d3d11.h>
     #include <dwmapi.h>
    +#include <Windows.h>
    +#include <wrl/client.h>
     
     #include <filesystem>
     #include <sstream>
    +#include <processthreadsapi.h>
     
     #include "flutter/fml/logging.h"
     #include "flutter/fml/paths.h"
    +#include "flutter/fml/thread.h"
     #include "flutter/fml/platform/win/wstring_conversion.h"
     #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h"
     #include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h"
    @@ -26,21 +30,48 @@
     // winbase.h defines GetCurrentTime as a macro.
     #undef GetCurrentTime
     
    +static constexpr double kMicrosecondsPerSecond = 1000000.0;
    +static constexpr int64_t kQPCOverflowThreshold = INT64_C(0x8637BD05AF7);
    +
     static constexpr char kAccessibilityChannelName[] = "flutter/accessibility";
     
     namespace flutter {
     
     namespace {
     
    -// Lifted from vsync_waiter_fallback.cc
    -static std::chrono::nanoseconds SnapToNextTick(
    -    std::chrono::nanoseconds value,
    -    std::chrono::nanoseconds tick_phase,
    -    std::chrono::nanoseconds tick_interval) {
    -  std::chrono::nanoseconds offset = (tick_phase - value) % tick_interval;
    -  if (offset != std::chrono::nanoseconds::zero())
    -    offset = offset + tick_interval;
    -  return value + offset;
    +
    +Microsoft::WRL::ComPtr<IDXGIOutput> DXGIOutputFromMonitor(
    +    HMONITOR monitor,
    +    const Microsoft::WRL::ComPtr<ID3D11Device>& d3d11_device) {
    +  Microsoft::WRL::ComPtr<IDXGIDevice> dxgi_device;
    +  if (FAILED(d3d11_device.As(&dxgi_device))) {
    +    FML_LOG(ERROR) << "Failed to retrieve DXGI device";
    +    return nullptr;
    +  }
    +
    +  Microsoft::WRL::ComPtr<IDXGIAdapter> dxgi_adapter;
    +  if (FAILED(dxgi_device->GetAdapter(&dxgi_adapter))) {
    +    FML_LOG(ERROR) << "Failed to retrieve DXGI adapter";
    +    return nullptr;
    +  }
    +
    +  size_t i = 0;
    +  while (true) {
    +    Microsoft::WRL::ComPtr<IDXGIOutput> output;
    +    if (FAILED(dxgi_adapter->EnumOutputs(i++, &output)))
    +      break;
    +
    +    DXGI_OUTPUT_DESC desc = {};
    +    if (FAILED(output->GetDesc(&desc))) {
    +      FML_LOG(ERROR) << "DXGI output GetDesc failed";
    +      return nullptr;
    +    }
    +
    +    if (desc.Monitor == monitor)
    +      return output;
    +  }
    +
    +  return nullptr;
     }
     
     // Creates and returns a FlutterRendererConfig that renders to the view (if any)
    @@ -335,10 +366,74 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
         auto host = static_cast<FlutterWindowsEngine*>(user_data);
         return host->HandlePlatformMessage(engine_message);
       };
    +  vsync_batons_ = {};
       args.vsync_callback = [](void* user_data, intptr_t baton) -> void {
         auto host = static_cast<FlutterWindowsEngine*>(user_data);
         host->OnVsync(baton);
    +    {
    +      std::scoped_lock lock{host->vsync_mutex_};
    +      host->vsync_batons_.push_back(baton);
    +      
    +    }
    +    //host->vsync_latch_.Signal();
       };
    +
    +  std::thread([this]() {
    +    ::SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
    +
    +    ::SetThreadDescription(GetCurrentThread(), L"vsyncThread");
    +
    +    LARGE_INTEGER ticks_per_sec = {};
    +    if (::QueryPerformanceFrequency(&ticks_per_sec)) {
    +      qpc_ticks_per_second_ = ticks_per_sec.QuadPart;
    +    }
    +
    +    Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device =
    +        reinterpret_cast<ID3D11Device*>(
    +            surface_manager_->QueryDeviceObject(EGL_D3D11_DEVICE_ANGLE));
    +
    +    while (true) {
    +      //vsync_latch_.Wait();
    +
    +      const HMONITOR monitor =
    +          ::MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
    +      if (primary_monitor_ != monitor) {
    +        primary_monitor_ = monitor;
    +        primary_output_ = DXGIOutputFromMonitor(monitor, d3d11_device);
    +      }
    +
    +      std::chrono::nanoseconds frame_start_time, interval;
    +
    +      std::tie(frame_start_time, interval) = GetVsyncTimingInfo();
    +
    +      primary_output_->WaitForVBlank();
    +
    + 
    +      std::vector<intptr_t> vsync_batons;
    +      {
    +        std::scoped_lock lock{vsync_mutex_};
    +        vsync_batons = std::move(vsync_batons_);
    +        vsync_batons_.clear();
    +      }
    +
    +      if (frame_start_time.count() == 0) {
    +        frame_start_time =
    +            std::chrono::nanoseconds(embedder_api_.GetCurrentTime());
    +      }
    +      auto vsync_time =
    +          std::chrono::nanoseconds(embedder_api_.GetCurrentTime());
    +
    +      frame_start_time = vsync_time;
    +      //interval = std::chrono::nanoseconds(16600000);
    +      std::chrono::nanoseconds frame_target_time = frame_start_time + interval;
    +
    +      for (intptr_t baton : vsync_batons) {
    +        embedder_api_.OnVsync(engine_, baton, frame_start_time.count(),
    +                              frame_target_time.count());
    +      }
    +    }
    +  }).detach();
    +
       args.on_pre_engine_restart_callback = [](void* user_data) {
         auto host = static_cast<FlutterWindowsEngine*>(user_data);
         host->OnPreEngineRestart();
    @@ -422,8 +517,10 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) {
       display.struct_size = sizeof(FlutterEngineDisplay);
       display.display_id = 0;
       display.single_display = true;
    -  display.refresh_rate =
    -      1.0 / (static_cast<double>(FrameInterval().count()) / 1000000000.0);
    +  //display.refresh_rate =
    +  //    1.0 / (static_cast<double>(FrameInterval().count()) / 1000000000.0);
    +  display.refresh_rate = 1.0 / (static_cast<double>(GetVsyncTimingInfo().second.count()) / kMicrosecondsPerSecond);
    +
     
       std::vector<FlutterEngineDisplay> displays = {display};
       embedder_api_.NotifyDisplayUpdate(engine_,
    @@ -457,33 +554,83 @@ void FlutterWindowsEngine::SetView(FlutterWindowsView* view) {
       InitializeKeyboard();
     }
     
    +
     void FlutterWindowsEngine::OnVsync(intptr_t baton) {
    -  std::chrono::nanoseconds current_time =
    -      std::chrono::nanoseconds(embedder_api_.GetCurrentTime());
    -  std::chrono::nanoseconds frame_interval = FrameInterval();
    -  auto next = SnapToNextTick(current_time, start_time_, frame_interval);
    -  embedder_api_.OnVsync(engine_, baton, next.count(),
    -                        (next + frame_interval).count());
    +
     }
     
    -std::chrono::nanoseconds FlutterWindowsEngine::FrameInterval() {
    -  if (frame_interval_override_.has_value()) {
    -    return frame_interval_override_.value();
    -  }
    -  uint64_t interval = 16600000;
    +std::pair<std::chrono::nanoseconds, std::chrono::nanoseconds>
    +FlutterWindowsEngine::GetVsyncTimingInfo() {
    +  // if (frame_interval_override_.has_value()) {
    +  //   return frame_interval_override_.value();
    +  // }
     
    +  uint64_t timebase = 0;
    +  uint64_t interval = 16600;
       DWM_TIMING_INFO timing_info = {};
       timing_info.cbSize = sizeof(timing_info);
       HRESULT result = DwmGetCompositionTimingInfo(NULL, &timing_info);
    -  if (result == S_OK && timing_info.rateRefresh.uiDenominator > 0 &&
    -      timing_info.rateRefresh.uiNumerator > 0) {
    -    interval = static_cast<double>(timing_info.rateRefresh.uiDenominator *
    -                                   1000000000.0) /
    -               static_cast<double>(timing_info.rateRefresh.uiNumerator);
    -  }
    +  if (result == S_OK) {
    +    uint64_t rate_interval;
    +    if (timing_info.rateRefresh.uiDenominator > 0 &&
    +        timing_info.rateRefresh.uiNumerator > 0) {
    +      rate_interval = timing_info.rateRefresh.uiDenominator *
    +                      kMicrosecondsPerSecond /
    +                      timing_info.rateRefresh.uiNumerator;
    +    }
     
    -  return std::chrono::nanoseconds(interval);
    -}
    +    bool is_high_resolution = qpc_ticks_per_second_ > 0;
    +    if (is_high_resolution) {
    +      timebase =
    +          QPCValueToMicroseconds(static_cast<LONGLONG>(timing_info.qpcVBlank));
    +
    +      interval = QPCValueToMicroseconds(
    +          static_cast<LONGLONG>(timing_info.qpcRefreshPeriod));
    +
    +      // Check for interval values that are impossibly low. A 29 microsecond
    +      // interval was seen (from a qpcRefreshPeriod of 60).
    +      if (interval < 1000 /*base::Milliseconds(1)*/) {
    +        interval = rate_interval;
    +      }
    +      // Check for the qpcRefreshPeriod interval being improbably small
    +      // compared to the rateRefresh calculated interval, as another
    +      // attempt at detecting driver bugs.
    +      if (rate_interval > 0 && interval < rate_interval / 2) {
    +        interval = rate_interval;
    +      }
    +    } else {
    +      // If FrameTime is not high resolution, we do not want to translate
    +      // the QPC value provided by DWM into the low-resolution timebase,
    +      // which would be error prone and jittery. As a fallback, we assume
    +      // the timebase is zero and use rateRefresh, which may be rounded but
    +      // isn't noisy like qpcRefreshPeriod, instead. The fact that we don't
    +      // have a timebase here may lead to brief periods of jank when our
    +      // scheduling becomes offset from the hardware vsync.
    +      interval = rate_interval;
    +    }
    +    }
    +    return std::make_pair(std::chrono::duration_cast<std::chrono::nanoseconds>(
    +                              std::chrono::microseconds(timebase)),
    +                          std::chrono::duration_cast<std::chrono::nanoseconds>(
    +                              std::chrono::microseconds(interval)));
    + }
    +
    +  int64_t FlutterWindowsEngine::QPCValueToMicroseconds(LONGLONG qpc_value) {
    +    BASE_DCHECK(qpc_ticks_per_second_ > 0);
    +
    +    if (qpc_value < kQPCOverflowThreshold) {
    +      return qpc_value * kMicrosecondsPerSecond / qpc_ticks_per_second_;
    +    } else {
    +      // Otherwise, calculate microseconds in a round about manner to avoid
    +      // overflow and precision issues.
    +      int64_t whole_seconds = qpc_value / qpc_ticks_per_second_;
    +      int64_t leftover_ticks =
    +          qpc_value - (whole_seconds * qpc_ticks_per_second_);
    +      return (whole_seconds * kMicrosecondsPerSecond) +
    +             ((leftover_ticks * kMicrosecondsPerSecond) /
    +              qpc_ticks_per_second_);
    +    }
    +  }
     
     // Returns the currently configured Plugin Registrar.
     FlutterDesktopPluginRegistrarRef FlutterWindowsEngine::GetRegistrar() {
    diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h
    index 04d33e7583..dcb5a489bc 100644
    --- a/shell/platform/windows/flutter_windows_engine.h
    +++ b/shell/platform/windows/flutter_windows_engine.h
    @@ -15,6 +15,7 @@
     
     #include "flutter/fml/closure.h"
     #include "flutter/fml/macros.h"
    +#include "flutter/fml/synchronization/waitable_event.h"
     #include "flutter/shell/platform/common/accessibility_bridge.h"
     #include "flutter/shell/platform/common/app_lifecycle_state.h"
     #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h"
    @@ -387,7 +388,11 @@ class FlutterWindowsEngine {
           plugin_registrar_destruction_callbacks_;
     
       // The approximate time between vblank events.
    -  std::chrono::nanoseconds FrameInterval();
    +  //std::chrono::nanoseconds FrameInterval();
    +  std::pair<std::chrono::nanoseconds, std::chrono::nanoseconds>
    +  GetVsyncTimingInfo();
    +
    +  int64_t QPCValueToMicroseconds(LONGLONG qpc_value);
     
       // The start time used to align frames.
       std::chrono::nanoseconds start_time_ = std::chrono::nanoseconds::zero();
    @@ -417,6 +422,13 @@ class FlutterWindowsEngine {
       WindowsProcTable windows_proc_table_;
     
       FML_DISALLOW_COPY_AND_ASSIGN(FlutterWindowsEngine);
    +
    +  int64_t qpc_ticks_per_second_ = 0;
    +  Microsoft::WRL::ComPtr<IDXGIOutput> primary_output_;
    +  HMONITOR primary_monitor_ = NULL;
    +  std::mutex vsync_mutex_;
    +  fml::AutoResetWaitableEvent vsync_latch_;
    +  std::vector<intptr_t> vsync_batons_;
     };
     
     }  // namespace flutter
    
  12. kobutri commented on Jan 31, 2024

    @kobutri

    I only had time yet to test the simple example of a moving bar, but it seems to be working pretty well. Thank you so much for your effort. I think landing this will remove one of the major barriers to making Flutter a viable choice on Windows.

    Edit: you may want to download the video file and play it locally. Because the playback seems laggy, at least in my browser.

    2024-01-31.13-03-32.mp4
  13. added
    team-windowsOwned by the Windows platform team
    and removed on Jun 6, 2024
  14. flutter-triage-bot commented on Jun 6, 2024

    @flutter-triage-bot

    The triaged-desktop label is irrelevant if there is no team-desktop label or fyi-desktop label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: desktopRunning on desktopc: performanceRelates to speed or footprint issues (see "perf:" labels)customer: productengineflutter/engine related. See also e: labels.perf: speedPerformance issues related to (mostly rendering) speedplatform-windowsBuilding on or for Windows specificallyteam-windowsOwned by the Windows platform teamtriaged-windowsTriaged by the Windows platform team

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions