diff --git a/shell/platform/windows/client_wrapper/flutter_engine.cc b/shell/platform/windows/client_wrapper/flutter_engine.cc index f35c20cd9e5a2..00ccbbcff7cab 100644 --- a/shell/platform/windows/client_wrapper/flutter_engine.cc +++ b/shell/platform/windows/client_wrapper/flutter_engine.cc @@ -88,6 +88,18 @@ FlutterDesktopPluginRegistrarRef FlutterEngine::GetRegistrarForPlugin( return FlutterDesktopEngineGetPluginRegistrar(engine_, plugin_name.c_str()); } +void FlutterEngine::SetNextFrameCallback(std::function callback) { + next_frame_callback_ = std::move(callback); + FlutterDesktopEngineSetNextFrameCallback( + engine_, + [](void* user_data) { + FlutterEngine* self = static_cast(user_data); + self->next_frame_callback_(); + self->next_frame_callback_ = nullptr; + }, + this); +} + FlutterDesktopEngineRef FlutterEngine::RelinquishEngine() { owns_engine_ = false; return engine_; diff --git a/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc index 6f09bb960aac5..a1274998eadce 100644 --- a/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc +++ b/shell/platform/windows/client_wrapper/flutter_engine_unittests.cc @@ -46,6 +46,13 @@ class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi { // |flutter::testing::StubFlutterWindowsApi| uint64_t EngineProcessMessages() override { return 99; } + // |flutter::testing::StubFlutterWindowsApi| + void EngineSetNextFrameCallback(VoidCallback callback, + void* user_data) override { + next_frame_callback_ = callback; + next_frame_user_data_ = user_data; + } + // |flutter::testing::StubFlutterWindowsApi| void EngineReloadSystemFonts() override { reload_fonts_called_ = true; } @@ -61,12 +68,20 @@ class TestFlutterWindowsApi : public testing::StubFlutterWindowsApi { return dart_entrypoint_arguments_; } + bool has_next_frame_callback() { return next_frame_callback_ != nullptr; } + void run_next_frame_callback() { + next_frame_callback_(next_frame_user_data_); + next_frame_callback_ = nullptr; + } + private: bool create_called_ = false; bool run_called_ = false; bool destroy_called_ = false; bool reload_fonts_called_ = false; std::vector dart_entrypoint_arguments_; + VoidCallback next_frame_callback_ = nullptr; + void* next_frame_user_data_ = nullptr; }; } // namespace @@ -168,4 +183,22 @@ TEST(FlutterEngineTest, DartEntrypointArgs) { EXPECT_TRUE(arguments[1] == arguments_ref[1]); } +TEST(FlutterEngineTest, SetNextFrameCallback) { + DartProject project(L"data"); + testing::ScopedStubFlutterWindowsApi scoped_api_stub( + std::make_unique()); + auto test_api = static_cast(scoped_api_stub.stub()); + + FlutterEngine engine(DartProject(L"fake/project/path")); + + bool success = false; + engine.SetNextFrameCallback([&success]() { success = true; }); + + EXPECT_TRUE(test_api->has_next_frame_callback()); + + test_api->run_next_frame_callback(); + + EXPECT_TRUE(success); +} + } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h index 897fccfe0eb94..63a820ce2d6bd 100644 --- a/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h +++ b/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -78,6 +78,12 @@ class FlutterEngine : public PluginRegistry { // This pointer will remain valid for the lifetime of this instance. BinaryMessenger* messenger() { return messenger_.get(); } + // Schedule a callback to be called after the next frame is drawn. + // + // This must be called from the platform thread. The callback is executed only + // once on the platform thread. + void SetNextFrameCallback(std::function callback); + private: // For access to RelinquishEngine. friend class FlutterViewController; @@ -101,6 +107,9 @@ class FlutterEngine : public PluginRegistry { // or if RelinquishEngine has been called (since the view controller will // run the engine if it hasn't already been run). bool has_been_run_ = false; + + // The callback to execute once the next frame is drawn. + std::function next_frame_callback_ = nullptr; }; } // namespace flutter diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index a829326a618c0..488cc49b8935b 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -111,6 +111,14 @@ uint64_t FlutterDesktopEngineProcessMessages(FlutterDesktopEngineRef engine) { return 0; } +void FlutterDesktopEngineSetNextFrameCallback(FlutterDesktopEngineRef engine, + VoidCallback callback, + void* user_data) { + if (s_stub_implementation) { + s_stub_implementation->EngineSetNextFrameCallback(callback, user_data); + } +} + void FlutterDesktopEngineReloadSystemFonts(FlutterDesktopEngineRef engine) { if (s_stub_implementation) { s_stub_implementation->EngineReloadSystemFonts(); diff --git a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index 125fb3614fbec..adc117a838f18 100644 --- a/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -62,6 +62,10 @@ class StubFlutterWindowsApi { // Called for FlutterDesktopEngineProcessMessages. virtual uint64_t EngineProcessMessages() { return 0; } + // Called for FlutterDesktopEngineSetNextFrameCallback. + virtual void EngineSetNextFrameCallback(VoidCallback callback, + void* user_data) {} + // Called for FlutterDesktopEngineReloadSystemFonts. virtual void EngineReloadSystemFonts() {} diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart index 2799c2f47ecf5..e2d76aa6171e7 100644 --- a/shell/platform/windows/fixtures/main.dart +++ b/shell/platform/windows/fixtures/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' as ui; + // Signals a waiting latch in the native test. void signal() native 'Signal'; @@ -11,6 +13,9 @@ void signalBoolValue(bool value) native 'SignalBoolValue'; // Signals a waiting latch in the native test, which returns a value to the fixture. bool signalBoolReturn() native 'SignalBoolReturn'; +// Notify the native test that the first frame has been scheduled. +void notifyFirstFrameScheduled() native 'NotifyFirstFrameScheduled'; + void main() { } @@ -33,3 +38,29 @@ void verifyNativeFunctionWithReturn() { bool value = signalBoolReturn(); signalBoolValue(value); } + +@pragma('vm:entry-point') +void drawHelloWorld() { + ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) { + final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle()) + ..addText('Hello world'); + final ui.Paragraph paragraph = paragraphBuilder.build(); + + paragraph.layout(const ui.ParagraphConstraints(width: 800.0)); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + + canvas.drawParagraph(paragraph, ui.Offset.zero); + + final ui.Picture picture = recorder.endRecording(); + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder() + ..addPicture(ui.Offset.zero, picture) + ..pop(); + + ui.window.render(sceneBuilder.build()); + }; + + ui.PlatformDispatcher.instance.scheduleFrame(); + notifyFirstFrameScheduled(); +} diff --git a/shell/platform/windows/flutter_windows.cc b/shell/platform/windows/flutter_windows.cc index 87c6c8bd55df4..c26157e7c71c7 100644 --- a/shell/platform/windows/flutter_windows.cc +++ b/shell/platform/windows/flutter_windows.cc @@ -176,6 +176,13 @@ FlutterDesktopTextureRegistrarRef FlutterDesktopEngineGetTextureRegistrar( EngineFromHandle(engine)->texture_registrar()); } +void FlutterDesktopEngineSetNextFrameCallback(FlutterDesktopEngineRef engine, + VoidCallback callback, + void* user_data) { + EngineFromHandle(engine)->SetNextFrameCallback( + [callback, user_data]() { callback(user_data); }); +} + HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view) { return ViewFromHandle(view)->GetPlatformWindow(); } diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc index 972b9bb94b254..6c3d728f7e4ff 100644 --- a/shell/platform/windows/flutter_windows_engine.cc +++ b/shell/platform/windows/flutter_windows_engine.cc @@ -506,6 +506,22 @@ void FlutterWindowsEngine::ScheduleFrame() { embedder_api_.ScheduleFrame(engine_); } +void FlutterWindowsEngine::SetNextFrameCallback(fml::closure callback) { + next_frame_callback_ = std::move(callback); + + embedder_api_.SetNextFrameCallback( + engine_, + [](void* user_data) { + // Embedder callback runs on raster thread. Switch back to platform + // thread. + FlutterWindowsEngine* self = + static_cast(user_data); + + self->task_runner_->PostTask(std::move(self->next_frame_callback_)); + }, + this); +} + void FlutterWindowsEngine::SendSystemLocales() { std::vector languages = GetPreferredLanguageInfo(); std::vector flutter_locales; diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h index c3c0d6109d922..3ead42238f616 100644 --- a/shell/platform/windows/flutter_windows_engine.h +++ b/shell/platform/windows/flutter_windows_engine.h @@ -177,6 +177,9 @@ class FlutterWindowsEngine { // Informs the engine that a new frame is needed to redraw the content. void ScheduleFrame(); + // Set the callback that is called when the next frame is drawn. + void SetNextFrameCallback(fml::closure callback); + // Attempts to register the texture with the given |texture_id|. bool RegisterExternalTexture(int64_t texture_id); @@ -294,6 +297,9 @@ class FlutterWindowsEngine { // The root isolate creation callback. fml::closure root_isolate_create_callback_; + + // The on frame drawn callback. + fml::closure next_frame_callback_; }; } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc index 191740f213ebd..87315efad58f5 100644 --- a/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -389,5 +389,20 @@ TEST(FlutterWindowsEngine, ScheduleFrame) { EXPECT_TRUE(called); } +TEST(FlutterWindowsEngine, SetNextFrameCallback) { + std::unique_ptr engine = GetTestEngine(); + EngineModifier modifier(engine.get()); + + bool called = false; + modifier.embedder_api().SetNextFrameCallback = MOCK_ENGINE_PROC( + SetNextFrameCallback, ([&called](auto engine, auto callback, auto data) { + called = true; + return kSuccess; + })); + + engine->SetNextFrameCallback([]() {}); + EXPECT_TRUE(called); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_unittests.cc b/shell/platform/windows/flutter_windows_unittests.cc index 4479e4cd02617..720c7b3fbc0a7 100644 --- a/shell/platform/windows/flutter_windows_unittests.cc +++ b/shell/platform/windows/flutter_windows_unittests.cc @@ -149,5 +149,59 @@ TEST_F(WindowsTest, VerifyNativeFunctionWithReturn) { EXPECT_TRUE(bool_value_passed); } +// Verify the next frame callback is executed. +TEST_F(WindowsTest, NextFrameCallback) { + struct Captures { + fml::AutoResetWaitableEvent frame_scheduled_latch; + fml::AutoResetWaitableEvent frame_drawn_latch; + std::thread::id thread_id; + }; + Captures captures; + + CreateNewThread("test_platform_thread")->PostTask([&]() { + captures.thread_id = std::this_thread::get_id(); + + auto& context = GetContext(); + WindowsConfigBuilder builder(context); + builder.SetDartEntrypoint("drawHelloWorld"); + + auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + ASSERT_FALSE(captures.frame_drawn_latch.IsSignaledForTest()); + captures.frame_scheduled_latch.Signal(); + }); + context.AddNativeFunction("NotifyFirstFrameScheduled", native_entry); + + ViewControllerPtr controller{builder.Run()}; + ASSERT_NE(controller, nullptr); + + auto engine = FlutterDesktopViewControllerGetEngine(controller.get()); + + FlutterDesktopEngineSetNextFrameCallback( + engine, + [](void* user_data) { + auto captures = static_cast(user_data); + + ASSERT_TRUE(captures->frame_scheduled_latch.IsSignaledForTest()); + + // Callback should execute on platform thread. + ASSERT_EQ(std::this_thread::get_id(), captures->thread_id); + + // Signal the test passed and end the Windows message loop. + captures->frame_drawn_latch.Signal(); + ::PostQuitMessage(0); + }, + &captures); + + // Pump messages for the Windows platform task runner. + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + }); + + captures.frame_drawn_latch.Wait(); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/public/flutter_windows.h b/shell/platform/windows/public/flutter_windows.h index ca1655f1e9f40..c5306beba0730 100644 --- a/shell/platform/windows/public/flutter_windows.h +++ b/shell/platform/windows/public/flutter_windows.h @@ -17,6 +17,8 @@ extern "C" { #endif +typedef void (*VoidCallback)(void* /* user data */); + // Opaque reference to a Flutter window controller. typedef struct FlutterDesktopViewControllerState* FlutterDesktopViewControllerRef; @@ -185,6 +187,15 @@ FlutterDesktopEngineGetMessenger(FlutterDesktopEngineRef engine); FLUTTER_EXPORT FlutterDesktopTextureRegistrarRef FlutterDesktopEngineGetTextureRegistrar(FlutterDesktopEngineRef engine); +// Schedule a callback to be called after the next frame is drawn. +// +// This must be called from the platform thread. The callback is executed only +// once on the platform thread. +FLUTTER_EXPORT void FlutterDesktopEngineSetNextFrameCallback( + FlutterDesktopEngineRef engine, + VoidCallback callback, + void* user_data); + // ========== View ========== // Return backing HWND for manipulation in host application.