diff --git a/shell/platform/windows/compositor_opengl.cc b/shell/platform/windows/compositor_opengl.cc index 3a445c4ded63f..9aef5f9b7fee1 100644 --- a/shell/platform/windows/compositor_opengl.cc +++ b/shell/platform/windows/compositor_opengl.cc @@ -11,6 +11,8 @@ namespace flutter { namespace { +constexpr uint32_t kWindowFrameBufferId = 0; + // The metadata for an OpenGL framebuffer backing store. struct FramebufferBackingStore { uint32_t framebuffer_id; @@ -91,32 +93,48 @@ bool CompositorOpenGL::CollectBackingStore(const FlutterBackingStore* store) { bool CompositorOpenGL::Present(const FlutterLayer** layers, size_t layers_count) { + if (!engine_->view()) { + return false; + } + + // Clear the view if there are no layers to present. + if (layers_count == 0) { + // Normally the compositor is initialized when the first backing store is + // created. However, on an empty frame no backing stores are created and + // the present needs to initialize the compositor. + if (!is_initialized_ && !Initialize()) { + return false; + } + + return ClearSurface(); + } + // TODO: Support compositing layers and platform views. // See: https://github.com/flutter/flutter/issues/31713 FML_DCHECK(is_initialized_); FML_DCHECK(layers_count == 1); + FML_DCHECK(layers[0]->offset.x == 0 && layers[0]->offset.y == 0); FML_DCHECK(layers[0]->type == kFlutterLayerContentTypeBackingStore); FML_DCHECK(layers[0]->backing_store->type == kFlutterBackingStoreTypeOpenGL); FML_DCHECK(layers[0]->backing_store->open_gl.type == kFlutterOpenGLTargetTypeFramebuffer); - if (!engine_->view()) { - return false; - } - auto width = layers[0]->size.width; auto height = layers[0]->size.height; - // Acquiring the view's framebuffer ID resizes its surface if necessary. - auto destination_id = engine_->view()->GetFrameBufferId(width, height); - auto source_id = layers[0]->backing_store->open_gl.framebuffer.name; + // Check if this frame can be presented. This resizes the surface if a resize + // is pending and |width| and |height| match the target size. + if (!engine_->view()->OnFrameGenerated(width, height)) { + return false; + } if (!engine_->surface_manager()->MakeCurrent()) { return false; } + auto source_id = layers[0]->backing_store->open_gl.framebuffer.name; gl_->BindFramebuffer(GL_READ_FRAMEBUFFER, source_id); - gl_->BindFramebuffer(GL_DRAW_FRAMEBUFFER, destination_id); + gl_->BindFramebuffer(GL_DRAW_FRAMEBUFFER, kWindowFrameBufferId); gl_->BlitFramebuffer(0, // srcX0 0, // srcY0 @@ -151,4 +169,20 @@ bool CompositorOpenGL::Initialize() { return true; } +bool CompositorOpenGL::ClearSurface() { + FML_DCHECK(is_initialized_); + + // Resize the surface if needed. + engine_->view()->OnEmptyFrameGenerated(); + + if (!engine_->surface_manager()->MakeCurrent()) { + return false; + } + + gl_->ClearColor(0.0f, 0.0f, 0.0f, 0.0f); + gl_->Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + return engine_->view()->SwapBuffers(); +} + } // namespace flutter diff --git a/shell/platform/windows/compositor_opengl.h b/shell/platform/windows/compositor_opengl.h index 46b42eebf0eb2..06c22202332e2 100644 --- a/shell/platform/windows/compositor_opengl.h +++ b/shell/platform/windows/compositor_opengl.h @@ -51,6 +51,9 @@ class CompositorOpenGL : public Compositor { // Initialize the compositor. This must run on the raster thread. bool Initialize(); + + // Clear the view's surface and removes any previously presented layers. + bool ClearSurface(); }; } // namespace flutter diff --git a/shell/platform/windows/compositor_opengl_unittests.cc b/shell/platform/windows/compositor_opengl_unittests.cc index c60d0c685e554..e6cd9039ba255 100644 --- a/shell/platform/windows/compositor_opengl_unittests.cc +++ b/shell/platform/windows/compositor_opengl_unittests.cc @@ -169,6 +169,20 @@ TEST_F(CompositorOpenGLTest, Present) { ASSERT_TRUE(compositor.CollectBackingStore(&backing_store)); } +TEST_F(CompositorOpenGLTest, PresentEmpty) { + UseEngineWithView(); + + auto compositor = CompositorOpenGL{engine(), kMockResolver}; + + // The context will be bound twice: first to initialize the compositor, second + // to clear the surface. + EXPECT_CALL(*surface_manager(), MakeCurrent) + .Times(2) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*view(), SwapBuffers).WillOnce(Return(true)); + EXPECT_TRUE(compositor.Present(nullptr, 0)); +} + TEST_F(CompositorOpenGLTest, HeadlessPresentIgnored) { UseHeadlessEngine(); diff --git a/shell/platform/windows/compositor_software.cc b/shell/platform/windows/compositor_software.cc index 06809e11b5ee2..6f6353cbe269c 100644 --- a/shell/platform/windows/compositor_software.cc +++ b/shell/platform/windows/compositor_software.cc @@ -39,17 +39,23 @@ bool CompositorSoftware::CollectBackingStore(const FlutterBackingStore* store) { bool CompositorSoftware::Present(const FlutterLayer** layers, size_t layers_count) { + if (!engine_->view()) { + return false; + } + + // Clear the view if there are no layers to present. + if (layers_count == 0) { + return engine_->view()->ClearSoftwareBitmap(); + } + // TODO: Support compositing layers and platform views. // See: https://github.com/flutter/flutter/issues/31713 FML_DCHECK(layers_count == 1); + FML_DCHECK(layers[0]->offset.x == 0 && layers[0]->offset.y == 0); FML_DCHECK(layers[0]->type == kFlutterLayerContentTypeBackingStore); FML_DCHECK(layers[0]->backing_store->type == kFlutterBackingStoreTypeSoftware); - if (!engine_->view()) { - return false; - } - const auto& backing_store = layers[0]->backing_store->software; return engine_->view()->PresentSoftwareBitmap( diff --git a/shell/platform/windows/compositor_software_unittests.cc b/shell/platform/windows/compositor_software_unittests.cc index e9a8860e03223..6665fa7cd70fb 100644 --- a/shell/platform/windows/compositor_software_unittests.cc +++ b/shell/platform/windows/compositor_software_unittests.cc @@ -30,6 +30,7 @@ class MockFlutterWindowsView : public FlutterWindowsView { PresentSoftwareBitmap, (const void* allocation, size_t row_bytes, size_t height), (override)); + MOCK_METHOD(bool, ClearSoftwareBitmap, (), (override)); private: FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView); @@ -105,6 +106,15 @@ TEST_F(CompositorSoftwareTest, Present) { ASSERT_TRUE(compositor.CollectBackingStore(&backing_store)); } +TEST_F(CompositorSoftwareTest, PresentEmpty) { + UseEngineWithView(); + + auto compositor = CompositorSoftware{engine()}; + + EXPECT_CALL(*view(), ClearSoftwareBitmap).WillOnce(Return(true)); + EXPECT_TRUE(compositor.Present(nullptr, 0)); +} + TEST_F(CompositorSoftwareTest, HeadlessPresentIgnored) { UseHeadlessEngine(); diff --git a/shell/platform/windows/flutter_window.cc b/shell/platform/windows/flutter_window.cc index 831faeab9cda8..89eca62db4a7e 100644 --- a/shell/platform/windows/flutter_window.cc +++ b/shell/platform/windows/flutter_window.cc @@ -322,6 +322,13 @@ void FlutterWindow::OnResetImeComposing() { AbortImeComposing(); } +bool FlutterWindow::OnBitmapSurfaceCleared() { + HDC dc = ::GetDC(GetWindowHandle()); + bool result = ::PatBlt(dc, 0, 0, current_width_, current_height_, BLACKNESS); + ::ReleaseDC(GetWindowHandle(), dc); + return result; +} + bool FlutterWindow::OnBitmapSurfaceUpdated(const void* allocation, size_t row_bytes, size_t height) { @@ -334,8 +341,8 @@ bool FlutterWindow::OnBitmapSurfaceUpdated(const void* allocation, bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; bmi.bmiHeader.biSizeImage = 0; - int ret = SetDIBitsToDevice(dc, 0, 0, row_bytes / 4, height, 0, 0, 0, height, - allocation, &bmi, DIB_RGB_COLORS); + int ret = ::SetDIBitsToDevice(dc, 0, 0, row_bytes / 4, height, 0, 0, 0, + height, allocation, &bmi, DIB_RGB_COLORS); ::ReleaseDC(GetWindowHandle(), dc); return ret != 0; } diff --git a/shell/platform/windows/flutter_window.h b/shell/platform/windows/flutter_window.h index f758c0e71ac20..8846dd051e076 100644 --- a/shell/platform/windows/flutter_window.h +++ b/shell/platform/windows/flutter_window.h @@ -178,6 +178,9 @@ class FlutterWindow : public KeyboardManager::WindowDelegate, // |FlutterWindowBindingHandler| virtual void OnWindowResized() override; + // |FlutterWindowBindingHandler| + virtual bool OnBitmapSurfaceCleared() override; + // |FlutterWindowBindingHandler| virtual bool OnBitmapSurfaceUpdated(const void* allocation, size_t row_bytes, diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index b964a6fca2aec..ae2670d684e8e 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -104,12 +104,28 @@ void FlutterWindowsView::SetEngine(FlutterWindowsEngine* engine) { binding_handler_->GetDpiScale()); } -uint32_t FlutterWindowsView::GetFrameBufferId(size_t width, size_t height) { - // Called on an engine-controlled (non-platform) thread. +void FlutterWindowsView::OnEmptyFrameGenerated() { + // Called on the raster thread. std::unique_lock lock(resize_mutex_); if (resize_status_ != ResizeState::kResizeStarted) { - return kWindowFrameBufferID; + return; + } + + // Platform thread is blocked for the entire duration until the + // resize_status_ is set to kDone. + engine_->surface_manager()->ResizeSurface( + GetRenderTarget(), resize_target_width_, resize_target_height_, + binding_handler_->NeedsVSync()); + resize_status_ = ResizeState::kFrameGenerated; +} + +bool FlutterWindowsView::OnFrameGenerated(size_t width, size_t height) { + // Called on the raster thread. + std::unique_lock lock(resize_mutex_); + + if (resize_status_ != ResizeState::kResizeStarted) { + return true; } if (resize_target_width_ == width && resize_target_height_ == height) { @@ -118,9 +134,10 @@ uint32_t FlutterWindowsView::GetFrameBufferId(size_t width, size_t height) { engine_->surface_manager()->ResizeSurface(GetRenderTarget(), width, height, binding_handler_->NeedsVSync()); resize_status_ = ResizeState::kFrameGenerated; + return true; } - return kWindowFrameBufferID; + return false; } void FlutterWindowsView::UpdateFlutterCursor(const std::string& cursor_name) { @@ -602,6 +619,10 @@ bool FlutterWindowsView::SwapBuffers() { } } +bool FlutterWindowsView::ClearSoftwareBitmap() { + return binding_handler_->OnBitmapSurfaceCleared(); +} + bool FlutterWindowsView::PresentSoftwareBitmap(const void* allocation, size_t row_bytes, size_t height) { diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index 2551fe8f0b3c7..ba5d3222d7dda 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -26,9 +26,6 @@ namespace flutter { -// ID for the window frame buffer. -inline constexpr uint32_t kWindowFrameBufferID = 0; - // An OS-windowing neutral abstration for a Flutter view that works // with win32 HWNDs. class FlutterWindowsView : public WindowBindingHandlerDelegate { @@ -68,11 +65,17 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { // Swap the view's surface buffers. Must be called on the engine's raster // thread. Returns true if the buffers were swapped. // + // |OnFrameGenerated| or |OnEmptyFrameGenerated| must be called before this + // method. + // // If the view is resizing, this returns false if the frame is not the target // size. Otherwise, it unblocks the platform thread and blocks the raster // thread until the v-blank. virtual bool SwapBuffers(); + // Callback to clear a previously presented software bitmap. + virtual bool ClearSoftwareBitmap(); + // Callback for presenting a software bitmap. virtual bool PresentSoftwareBitmap(const void* allocation, size_t row_bytes, @@ -87,8 +90,18 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { // |WindowBindingHandlerDelegate| void OnHighContrastChanged() override; - // Returns the frame buffer id for the engine to render to. - uint32_t GetFrameBufferId(size_t width, size_t height); + // Called on the raster thread when |CompositorOpenGL| receives an empty + // frame. + // + // This resizes the surface if a resize is pending. + void OnEmptyFrameGenerated(); + + // Called on the raster thread when |CompositorOpenGL| receives a frame. + // Returns true if the frame can be presented. + // + // This resizes the surface if a resize is pending and |width| and + // |height| match the target size. + bool OnFrameGenerated(size_t width, size_t height); // Sets the cursor that should be used when the mouse is over the Flutter // content. See mouse_cursor.dart for the values and meanings of cursor_name. diff --git a/shell/platform/windows/flutter_windows_view_unittests.cc b/shell/platform/windows/flutter_windows_view_unittests.cc index de1379bf55290..4e2ec96e63b34 100644 --- a/shell/platform/windows/flutter_windows_view_unittests.cc +++ b/shell/platform/windows/flutter_windows_view_unittests.cc @@ -849,8 +849,55 @@ TEST(FlutterWindowsViewTest, WindowResizeTests) { // 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); + // Complete the window resize by reporting a frame with the new window size. + view.OnFrameGenerated(500, 500); + resized_latch.Wait(); +} + +// Verify that an empty frame completes a view resize. +TEST(FlutterWindowsViewTest, TestEmptyFrameResizes) { + std::unique_ptr engine = GetTestEngine(); + EngineModifier modifier(engine.get()); + + auto window_binding_handler = + std::make_unique>(); + std::unique_ptr surface_manager = + std::make_unique(); + + 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(std::move(surface_manager)); + view.SetEngine(engine.get()); + + fml::AutoResetWaitableEvent metrics_sent_latch; + modifier.embedder_api().SendWindowMetricsEvent = MOCK_ENGINE_PROC( + SendWindowMetricsEvent, + ([&metrics_sent_latch](auto engine, + const FlutterWindowMetricsEvent* event) { + metrics_sent_latch.Signal(); + return kSuccess; + })); + + 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); + resized_latch.Signal(); + }).detach(); + + // Wait until the platform thread has started the window resize. + metrics_sent_latch.Wait(); + + // Complete the window resize by reporting a frame with the new window size. + view.OnEmptyFrameGenerated(); resized_latch.Wait(); } diff --git a/shell/platform/windows/testing/mock_window_binding_handler.h b/shell/platform/windows/testing/mock_window_binding_handler.h index 43be5abc71a43..55ec886e8159f 100644 --- a/shell/platform/windows/testing/mock_window_binding_handler.h +++ b/shell/platform/windows/testing/mock_window_binding_handler.h @@ -33,6 +33,7 @@ class MockWindowBindingHandler : public WindowBindingHandler { MOCK_METHOD(void, SetFlutterCursor, (HCURSOR cursor_name), (override)); MOCK_METHOD(void, OnCursorRectUpdated, (const Rect& rect), (override)); MOCK_METHOD(void, OnResetImeComposing, (), (override)); + MOCK_METHOD(bool, OnBitmapSurfaceCleared, (), (override)); MOCK_METHOD(bool, OnBitmapSurfaceUpdated, (const void* allocation, size_t row_bytes, size_t height), diff --git a/shell/platform/windows/window_binding_handler.h b/shell/platform/windows/window_binding_handler.h index 390e9235c5f23..76b2100c99e8b 100644 --- a/shell/platform/windows/window_binding_handler.h +++ b/shell/platform/windows/window_binding_handler.h @@ -82,7 +82,12 @@ class WindowBindingHandler { // Invoked when the cursor/composing rect has been updated in the framework. virtual void OnCursorRectUpdated(const Rect& rect) = 0; - // Invoked when the Embedder provides us with new bitmap data for the contents + // Invoked when the embedder clears the contents of this Flutter view. + // + // Returns whether the surface was successfully updated or not. + virtual bool OnBitmapSurfaceCleared() = 0; + + // Invoked when the embedder provides us with new bitmap data for the contents // of this Flutter view. // // Returns whether the surface was successfully updated or not.