Skip to content

Commit

Permalink
[Windows] Don't block raster thread until v-blank (#41231)
Browse files Browse the repository at this point in the history
Currently the Windows raster thread blocks until the v-blank. As a result, on Windows a frame can spend 17ms on the raster thread even if rasterization completes in under 1ms. This behavior prevents us from rendering multiple views on the same raster thread on Windows.

This change does not make rasterization faster. Instead, it allows the raster thread to do more work. 

Addresses flutter/flutter#124903

### Results

Here are the performance graphs if I hover on and off the counter app's button repeatedly.

#### Before 
The raster thread spends ~17ms per frame consistently:

![Pasted image 20230414180144](https://user-images.githubusercontent.com/737941/232175444-f4b8eb64-b8a3-47b3-aa85-112c56d596a2.png)

#### After
After this change, the raster thread spends less than 1ms per frame consistently:
![Pasted image 20230414180011](https://user-images.githubusercontent.com/737941/232176377-a2f26c41-dfd1-4140-a775-59d6f0fac31f.png)

### Background

Blocking until the v-blank prevents screen tearing if the OS does not synchronize to the vsync. Windows does synchronize if the Desktop Windows Manager composition is enabled, which is required on Windows 8 and newer. In other words, this blocking behavior is unnecessary for Windows 8 and newer.

For Windows 7, screen tearing is possible if DWM composition is disabled (either by the user or by an app). In this scenario, the Flutter app should block until the v-blank to synchronize with the vsync and prevent screen tearing. We can detect if DWM composition is disabled using [`DwmIsCompositionEnabled`](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmiscompositionenabled). We can detect changes to DWM composition using the top-level [`WM_DWMCOMPOSITIONCHANGED`](https://learn.microsoft.com/en-us/windows/win32/dwm/wm-dwmcompositionchanged) Windows message.

Useful resources:
1. https://learn.microsoft.com/en-us/windows/win32/dwm/composition-ovw
2. https://www.khronos.org/opengl/wiki/Swap_Interval
3. https://forums.imgtec.com/t/eglswapinterval-0-tearing-problem/2356/5
4. https://stackoverflow.com/questions/32282252/can-eglswapinterval0-cause-screen-tearing
5. https://bugs.chromium.org/p/chromium/issues/detail?id=480361
  • Loading branch information
loic-sharma committed Apr 21, 2023
1 parent a2b02b8 commit 122c3b3
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 25 deletions.
29 changes: 26 additions & 3 deletions shell/platform/windows/angle_surface_manager.cc
Expand Up @@ -209,7 +209,8 @@ void AngleSurfaceManager::CleanUp() {

bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target,
EGLint width,
EGLint height) {
EGLint height,
bool vsync_enabled) {
if (!render_target || !initialize_succeeded_) {
return false;
}
Expand All @@ -226,17 +227,21 @@ bool AngleSurfaceManager::CreateSurface(WindowsRenderTarget* render_target,
surfaceAttributes);
if (surface == EGL_NO_SURFACE) {
LogEglError("Surface creation failed.");
return false;
}

surface_width_ = width;
surface_height_ = height;
render_surface_ = surface;

SetVSyncEnabled(vsync_enabled);
return true;
}

void AngleSurfaceManager::ResizeSurface(WindowsRenderTarget* render_target,
EGLint width,
EGLint height) {
EGLint height,
bool vsync_enabled) {
EGLint existing_width, existing_height;
GetSurfaceDimensions(&existing_width, &existing_height);
if (width != existing_width || height != existing_height) {
Expand All @@ -245,7 +250,7 @@ void AngleSurfaceManager::ResizeSurface(WindowsRenderTarget* render_target,

ClearContext();
DestroySurface();
if (!CreateSurface(render_target, width, height)) {
if (!CreateSurface(render_target, width, height, vsync_enabled)) {
FML_LOG(ERROR)
<< "AngleSurfaceManager::ResizeSurface failed to create surface";
}
Expand Down Expand Up @@ -300,6 +305,24 @@ EGLSurface AngleSurfaceManager::CreateSurfaceFromHandle(
egl_config_, attributes);
}

void AngleSurfaceManager::SetVSyncEnabled(bool enabled) {
if (eglMakeCurrent(egl_display_, render_surface_, render_surface_,
egl_context_) != EGL_TRUE) {
LogEglError("Unable to make surface current to update the swap interval");
return;
}

// OpenGL swap intervals can be used to prevent screen tearing.
// If enabled, the raster thread blocks until the v-blank.
// This is unnecessary if DWM composition is enabled.
// See: https://www.khronos.org/opengl/wiki/Swap_Interval
// See: https://learn.microsoft.com/windows/win32/dwm/composition-ovw
if (eglSwapInterval(egl_display_, enabled ? 1 : 0) != EGL_TRUE) {
LogEglError("Unable to update the swap interval");
return;
}
}

bool AngleSurfaceManager::GetDevice(ID3D11Device** device) {
if (!resolved_device_) {
PFNEGLQUERYDISPLAYATTRIBEXTPROC egl_query_display_attrib_EXT =
Expand Down
20 changes: 13 additions & 7 deletions shell/platform/windows/angle_surface_manager.h
Expand Up @@ -23,7 +23,7 @@

namespace flutter {

// A manager for inializing ANGLE correctly and using it to create and
// A manager for initializing ANGLE correctly and using it to create and
// destroy surfaces
class AngleSurfaceManager {
public:
Expand All @@ -34,17 +34,19 @@ class AngleSurfaceManager {
// associated with window, in the appropriate format for display.
// Target represents the visual entity to bind to. Width and
// height represent dimensions surface is created at.
bool CreateSurface(WindowsRenderTarget* render_target,
EGLint width,
EGLint height);
virtual bool CreateSurface(WindowsRenderTarget* render_target,
EGLint width,
EGLint height,
bool enable_vsync);

// Resizes backing surface from current size to newly requested size
// based on width and height for the specific case when width and height do
// not match current surface dimensions. Target represents the visual entity
// to bind to.
void ResizeSurface(WindowsRenderTarget* render_target,
EGLint width,
EGLint height);
virtual void ResizeSurface(WindowsRenderTarget* render_target,
EGLint width,
EGLint height,
bool enable_vsync);

// queries EGL for the dimensions of surface in physical
// pixels returning width and height as out params.
Expand Down Expand Up @@ -76,6 +78,10 @@ class AngleSurfaceManager {
// Gets the |EGLDisplay|.
EGLDisplay egl_display() const { return egl_display_; };

// If enabled, makes the current surface's buffer swaps block until the
// v-blank.
virtual void SetVSyncEnabled(bool enabled);

// Gets the |ID3D11Device| chosen by ANGLE.
bool GetDevice(ID3D11Device** device);

Expand Down
12 changes: 12 additions & 0 deletions shell/platform/windows/flutter_window.cc
Expand Up @@ -312,4 +312,16 @@ ui::AXPlatformNodeWin* FlutterWindow::GetAlert() {
return alert_node_.get();
}

bool FlutterWindow::NeedsVSync() {
// If the Desktop Window Manager composition is enabled,
// the system itself synchronizes with v-sync.
// See: https://learn.microsoft.com/windows/win32/dwm/composition-ovw
BOOL composition_enabled;
if (SUCCEEDED(::DwmIsCompositionEnabled(&composition_enabled))) {
return !composition_enabled;
}

return true;
}

} // namespace flutter
3 changes: 3 additions & 0 deletions shell/platform/windows/flutter_window.h
Expand Up @@ -156,6 +156,9 @@ class FlutterWindow : public Window, public WindowBindingHandler {
// |WindowBindingHandler|
ui::AXPlatformNodeWin* GetAlert() override;

// |WindowBindingHandler|
bool NeedsVSync() override;

// |Window|
ui::AXFragmentRootDelegateWin* GetAxFragmentRootDelegate() override;

Expand Down
4 changes: 4 additions & 0 deletions shell/platform/windows/flutter_windows_engine.cc
Expand Up @@ -774,4 +774,8 @@ FlutterWindowsEngine::accessibility_bridge() {
return view_->accessibility_bridge();
}

void FlutterWindowsEngine::OnDwmCompositionChanged() {
view_->OnDwmCompositionChanged();
}

} // namespace flutter
3 changes: 3 additions & 0 deletions shell/platform/windows/flutter_windows_engine.h
Expand Up @@ -266,6 +266,9 @@ class FlutterWindowsEngine {
LPARAM lparam,
AppExitType exit_type);

// Called when a WM_DWMCOMPOSITIONCHANGED message is received.
void OnDwmCompositionChanged();

protected:
// Creates the keyboard key handler.
//
Expand Down
14 changes: 11 additions & 3 deletions shell/platform/windows/flutter_windows_view.cc
Expand Up @@ -81,8 +81,8 @@ uint32_t FlutterWindowsView::GetFrameBufferId(size_t width, size_t height) {
if (resize_target_width_ == width && resize_target_height_ == height) {
// Platform thread is blocked for the entire duration until the
// resize_status_ is set to kDone.
engine_->surface_manager()->ResizeSurface(GetRenderTarget(), width, height);
engine_->surface_manager()->MakeCurrent();
engine_->surface_manager()->ResizeSurface(GetRenderTarget(), width, height,
binding_handler_->NeedsVSync());
resize_status_ = ResizeState::kFrameGenerated;
}

Expand Down Expand Up @@ -586,8 +586,10 @@ bool FlutterWindowsView::PresentSoftwareBitmap(const void* allocation,
void FlutterWindowsView::CreateRenderSurface() {
if (engine_ && engine_->surface_manager()) {
PhysicalWindowBounds bounds = binding_handler_->GetPhysicalWindowBounds();
bool enable_vsync = binding_handler_->NeedsVSync();
engine_->surface_manager()->CreateSurface(GetRenderTarget(), bounds.width,
bounds.height);
bounds.height, enable_vsync);

resize_target_width_ = bounds.width;
resize_target_height_ = bounds.height;
}
Expand Down Expand Up @@ -661,4 +663,10 @@ void FlutterWindowsView::UpdateSemanticsEnabled(bool enabled) {
}
}

void FlutterWindowsView::OnDwmCompositionChanged() {
if (engine_->surface_manager()) {
engine_->surface_manager()->SetVSyncEnabled(binding_handler_->NeedsVSync());
}
}

} // namespace flutter
3 changes: 3 additions & 0 deletions shell/platform/windows/flutter_windows_view.h
Expand Up @@ -195,6 +195,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
// |TextInputPluginDelegate|
void OnResetImeComposing() override;

// Called when a WM_ONCOMPOSITIONCHANGED message is received.
void OnDwmCompositionChanged();

// Get a pointer to the alert node for this view.
ui::AXPlatformNodeWin* AlertNode() const;

Expand Down
134 changes: 124 additions & 10 deletions shell/platform/windows/flutter_windows_view_unittests.cc
Expand Up @@ -12,6 +12,7 @@
#include <future>
#include <vector>

#include "flutter/fml/synchronization/waitable_event.h"
#include "flutter/shell/platform/common/json_message_codec.h"
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
#include "flutter/shell/platform/windows/flutter_window.h"
Expand All @@ -27,8 +28,10 @@
namespace flutter {
namespace testing {

using ::testing::_;
using ::testing::InSequence;
using ::testing::NiceMock;
using ::testing::Return;

constexpr uint64_t kScanCodeKeyA = 0x1e;
constexpr uint64_t kVirtualKeyA = 0x41;
Expand Down Expand Up @@ -117,8 +120,12 @@ class MockAngleSurfaceManager : public AngleSurfaceManager {
public:
MockAngleSurfaceManager() {}

MOCK_METHOD4(CreateSurface, bool(WindowsRenderTarget*, EGLint, EGLint, bool));
MOCK_METHOD4(ResizeSurface, void(WindowsRenderTarget*, EGLint, EGLint, bool));
MOCK_METHOD0(DestroySurface, void());

MOCK_METHOD1(SetVSyncEnabled, void(bool));

private:
FML_DISALLOW_COPY_AND_ASSIGN(MockAngleSurfaceManager);
};
Expand Down Expand Up @@ -714,28 +721,44 @@ TEST(FlutterWindowsViewTest, WindowResizeTests) {

auto window_binding_handler =
std::make_unique<NiceMock<MockWindowBindingHandler>>();
std::unique_ptr<MockAngleSurfaceManager> surface_manager =
std::make_unique<MockAngleSurfaceManager>();

EXPECT_CALL(*window_binding_handler.get(), NeedsVSync)
.WillOnce(Return(false));
EXPECT_CALL(
*surface_manager.get(),
ResizeSurface(_, /*width=*/500, /*height=*/500, /*enable_vsync=*/false))
.Times(1);
EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);

FlutterWindowsView view(std::move(window_binding_handler));
modifier.SetSurfaceManager(surface_manager.release());
view.SetEngine(std::move(engine));

bool send_window_metrics_event_called = false;
fml::AutoResetWaitableEvent metrics_sent_latch;
modifier.embedder_api().SendWindowMetricsEvent = MOCK_ENGINE_PROC(
SendWindowMetricsEvent,
([&send_window_metrics_event_called](
auto engine, const FlutterWindowMetricsEvent* even) {
send_window_metrics_event_called = true;
([&metrics_sent_latch](auto engine,
const FlutterWindowMetricsEvent* event) {
metrics_sent_latch.Signal();
return kSuccess;
}));

std::promise<bool> resize_completed;
std::thread([&resize_completed, &view]() {
fml::AutoResetWaitableEvent resized_latch;
std::thread([&resized_latch, &view]() {
// Start the window resize. This sends the new window metrics
// and then blocks until another thread completes the window resize.
view.OnWindowSizeChanged(500, 500);
resize_completed.set_value(true);
resized_latch.Signal();
}).detach();

auto result = resize_completed.get_future().wait_for(std::chrono::seconds(1));
EXPECT_EQ(std::future_status::ready, result);
EXPECT_TRUE(send_window_metrics_event_called);
// Wait until the platform thread has started the window resize.
metrics_sent_latch.Wait();

// Complete the window resize by requesting a buffer with the new window size.
view.GetFrameBufferId(500, 500);
resized_latch.Wait();
}

TEST(FlutterWindowsViewTest, WindowRepaintTests) {
Expand Down Expand Up @@ -1083,5 +1106,96 @@ TEST(FlutterWindowsViewTest, TooltipNodeData) {
EXPECT_EQ(uia_tooltip, "tooltip");
}

// Don't block until the v-blank if it is disabled by the window.
TEST(FlutterWindowsViewTest, DisablesVSync) {
std::unique_ptr<MockFlutterWindowsEngine> engine =
std::make_unique<MockFlutterWindowsEngine>();
auto window_binding_handler =
std::make_unique<NiceMock<MockWindowBindingHandler>>();
std::unique_ptr<MockAngleSurfaceManager> surface_manager =
std::make_unique<MockAngleSurfaceManager>();

EXPECT_CALL(*window_binding_handler.get(), NeedsVSync)
.WillOnce(Return(false));

EngineModifier modifier(engine.get());
FlutterWindowsView view(std::move(window_binding_handler));

InSequence s;
EXPECT_CALL(*surface_manager.get(),
CreateSurface(_, _, _, /*vsync_enabled=*/false))
.Times(1)
.WillOnce(Return(true));

EXPECT_CALL(*engine.get(), Stop).Times(1);
EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);

modifier.SetSurfaceManager(surface_manager.release());
view.SetEngine(std::move(engine));

view.CreateRenderSurface();
}

// Blocks until the v-blank if it is enabled by the window.
TEST(FlutterWindowsViewTest, EnablesVSync) {
std::unique_ptr<MockFlutterWindowsEngine> engine =
std::make_unique<MockFlutterWindowsEngine>();
auto window_binding_handler =
std::make_unique<NiceMock<MockWindowBindingHandler>>();
std::unique_ptr<MockAngleSurfaceManager> surface_manager =
std::make_unique<MockAngleSurfaceManager>();

EXPECT_CALL(*window_binding_handler.get(), NeedsVSync).WillOnce(Return(true));

EngineModifier modifier(engine.get());
FlutterWindowsView view(std::move(window_binding_handler));

InSequence s;
EXPECT_CALL(*surface_manager.get(),
CreateSurface(_, _, _, /*vsync_enabled=*/true))
.Times(1)
.WillOnce(Return(true));

EXPECT_CALL(*engine.get(), Stop).Times(1);
EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);

modifier.SetSurfaceManager(surface_manager.release());
view.SetEngine(std::move(engine));

view.CreateRenderSurface();
}

// Desktop Window Manager composition can be disabled on Windows 7.
// If this happens, the app must synchronize with the vsync to prevent
// screen tearing.
TEST(FlutterWindowsViewTest, UpdatesVSyncOnDwmUpdates) {
std::unique_ptr<MockFlutterWindowsEngine> engine =
std::make_unique<MockFlutterWindowsEngine>();
auto window_binding_handler =
std::make_unique<NiceMock<MockWindowBindingHandler>>();
std::unique_ptr<MockAngleSurfaceManager> surface_manager =
std::make_unique<MockAngleSurfaceManager>();

EXPECT_CALL(*window_binding_handler.get(), NeedsVSync)
.WillOnce(Return(true))
.WillOnce(Return(false));

EngineModifier modifier(engine.get());
FlutterWindowsView view(std::move(window_binding_handler));

InSequence s;
EXPECT_CALL(*surface_manager.get(), SetVSyncEnabled(true)).Times(1);
EXPECT_CALL(*surface_manager.get(), SetVSyncEnabled(false)).Times(1);

EXPECT_CALL(*engine.get(), Stop).Times(1);
EXPECT_CALL(*surface_manager.get(), DestroySurface).Times(1);

modifier.SetSurfaceManager(surface_manager.release());
view.SetEngine(std::move(engine));

view.GetEngine()->OnDwmCompositionChanged();
view.GetEngine()->OnDwmCompositionChanged();
}

} // namespace testing
} // namespace flutter
Expand Up @@ -36,6 +36,7 @@ class MockWindowBindingHandler : public WindowBindingHandler {
MOCK_METHOD0(SendInitialAccessibilityFeatures, void());
MOCK_METHOD0(GetAlertDelegate, AlertPlatformNodeDelegate*());
MOCK_METHOD0(GetAlert, ui::AXPlatformNodeWin*());
MOCK_METHOD0(NeedsVSync, bool());

private:
FML_DISALLOW_COPY_AND_ASSIGN(MockWindowBindingHandler);
Expand Down

0 comments on commit 122c3b3

Please sign in to comment.