diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index fcec10922825591251b2737bfc569781a6e04cb9..1418af878d6d973a689745b11efe29339839beac 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -918,6 +918,8 @@ FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/flutte FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/flutter_window_controller.h FILE: ../../../flutter/shell/platform/glfw/client_wrapper/include/flutter/plugin_registrar_glfw.h FILE: ../../../flutter/shell/platform/glfw/flutter_glfw.cc +FILE: ../../../flutter/shell/platform/glfw/glfw_event_loop.cc +FILE: ../../../flutter/shell/platform/glfw/glfw_event_loop.h FILE: ../../../flutter/shell/platform/glfw/key_event_handler.cc FILE: ../../../flutter/shell/platform/glfw/key_event_handler.h FILE: ../../../flutter/shell/platform/glfw/keyboard_hook_handler.h diff --git a/shell/platform/glfw/BUILD.gn b/shell/platform/glfw/BUILD.gn index defb5b707c168892cd277684a73b8ddd7c3834e2..95118a7cf6f7097a28cf0ade7e5b8211eb2bf7c3 100644 --- a/shell/platform/glfw/BUILD.gn +++ b/shell/platform/glfw/BUILD.gn @@ -33,6 +33,8 @@ source_set("flutter_glfw_headers") { source_set("flutter_glfw") { sources = [ "flutter_glfw.cc", + "glfw_event_loop.cc", + "glfw_event_loop.h", "key_event_handler.cc", "key_event_handler.h", "keyboard_hook_handler.h", @@ -65,6 +67,11 @@ source_set("flutter_glfw") { "$flutter_root/shell/platform/linux/config:gtk3", "$flutter_root/shell/platform/linux/config:x11", ] + } else if (is_mac) { + libs = [ + "CoreVideo.framework", + "IOKit.framework", + ] } } diff --git a/shell/platform/glfw/flutter_glfw.cc b/shell/platform/glfw/flutter_glfw.cc index 05cef2990e6bf2c3b2a6c2b596269100d22beee7..438c93506357ce17fc53f3fc7be5318a4c2deabc 100644 --- a/shell/platform/glfw/flutter_glfw.cc +++ b/shell/platform/glfw/flutter_glfw.cc @@ -15,6 +15,7 @@ #include "flutter/shell/platform/common/cpp/client_wrapper/include/flutter/plugin_registrar.h" #include "flutter/shell/platform/common/cpp/incoming_message_dispatcher.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/glfw/glfw_event_loop.h" #include "flutter/shell/platform/glfw/key_event_handler.h" #include "flutter/shell/platform/glfw/keyboard_hook_handler.h" #include "flutter/shell/platform/glfw/platform_handler.h" @@ -79,8 +80,11 @@ struct FlutterDesktopWindowControllerState { // Handler for the flutter/platform channel. std::unique_ptr platform_handler; - // Whether or not the pointer has been added (or if tracking is enabled, has - // been added since it was last removed). + // The event loop for the main thread that allows for delayed task execution. + std::unique_ptr event_loop; + + // Whether or not the pointer has been added (or if tracking is enabled, + // has been added since it was last removed). bool pointer_currently_added = false; // The screen coordinates per inch on the primary monitor. Defaults to a sane @@ -489,11 +493,13 @@ static void GLFWErrorCallback(int error_code, const char* description) { // provided). // // Returns a caller-owned pointer to the engine. -static FlutterEngine RunFlutterEngine(GLFWwindow* window, - const char* assets_path, - const char* icu_data_path, - const char** arguments, - size_t arguments_count) { +static FlutterEngine RunFlutterEngine( + GLFWwindow* window, + const char* assets_path, + const char* icu_data_path, + const char** arguments, + size_t arguments_count, + const FlutterCustomTaskRunners* custom_task_runners) { // FlutterProjectArgs is expecting a full argv, so when processing it for // flags the first item is treated as the executable and ignored. Add a dummy // value so that all provided arguments are used. @@ -528,6 +534,7 @@ static FlutterEngine RunFlutterEngine(GLFWwindow* window, args.command_line_argc = static_cast(argv.size()); args.command_line_argv = &argv[0]; args.platform_message_callback = GLFWOnFlutterPlatformMessage; + args.custom_task_runners = custom_task_runners; FlutterEngine engine = nullptr; auto result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, window, &engine); @@ -578,9 +585,38 @@ FlutterDesktopWindowControllerRef FlutterDesktopCreateWindow( // GLFWMakeResourceContextCurrent immediately. state->resource_window = CreateShareWindowForWindow(window); + // Create an event loop for the window. It is not running yet. + state->event_loop = std::make_unique( + std::this_thread::get_id(), // main GLFW thread + [state = state.get()](const auto* task) { + if (FlutterEngineRunTask(state->engine, task) != kSuccess) { + std::cerr << "Could not post an engine task." << std::endl; + } + }); + + // Configure task runner interop. + FlutterTaskRunnerDescription platform_task_runner = {}; + platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription); + platform_task_runner.user_data = state.get(); + platform_task_runner.runs_task_on_current_thread_callback = + [](void* state) -> bool { + return reinterpret_cast(state) + ->event_loop->RunsTasksOnCurrentThread(); + }; + platform_task_runner.post_task_callback = + [](FlutterTask task, uint64_t target_time_nanos, void* state) -> void { + reinterpret_cast(state) + ->event_loop->PostTask(task, target_time_nanos); + }; + + FlutterCustomTaskRunners custom_task_runners = {}; + custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); + custom_task_runners.platform_task_runner = &platform_task_runner; + // Start the engine. - state->engine = RunFlutterEngine(window, assets_path, icu_data_path, - arguments, argument_count); + state->engine = + RunFlutterEngine(window, assets_path, icu_data_path, arguments, + argument_count, &custom_task_runners); if (state->engine == nullptr) { return nullptr; } @@ -704,15 +740,17 @@ void FlutterDesktopRunWindowLoop(FlutterDesktopWindowControllerRef controller) { // Necessary for GTK thread safety. XInitThreads(); #endif + while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); + auto wait_duration = std::chrono::milliseconds::max(); #ifdef FLUTTER_USE_GTK + // If we are not using GTK, there is no point in waking up. + wait_duration = std::chrono::milliseconds(10); if (gtk_events_pending()) { gtk_main_iteration(); } #endif - // TODO(awdavies): This will be deprecated soon. - __FlutterEngineFlushPendingTasksNow(); + controller->event_loop->WaitForEvents(wait_duration); } FlutterDesktopDestroyWindow(controller); } @@ -738,8 +776,9 @@ FlutterDesktopEngineRef FlutterDesktopRunEngine(const char* assets_path, const char* icu_data_path, const char** arguments, size_t argument_count) { - auto engine = RunFlutterEngine(nullptr, assets_path, icu_data_path, arguments, - argument_count); + auto engine = + RunFlutterEngine(nullptr, assets_path, icu_data_path, arguments, + argument_count, nullptr /* custom task runners */); if (engine == nullptr) { return nullptr; } diff --git a/shell/platform/glfw/glfw_event_loop.cc b/shell/platform/glfw/glfw_event_loop.cc new file mode 100644 index 0000000000000000000000000000000000000000..a92bab50b5a26aa506d5a5ba92df757e25919f4b --- /dev/null +++ b/shell/platform/glfw/glfw_event_loop.cc @@ -0,0 +1,114 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/glfw/glfw_event_loop.h" + +#include + +#include +#include + +namespace flutter { + +GLFWEventLoop::GLFWEventLoop(std::thread::id main_thread_id, + TaskExpiredCallback on_task_expired) + : main_thread_id_(main_thread_id), + on_task_expired_(std::move(on_task_expired)) {} + +GLFWEventLoop::~GLFWEventLoop() = default; + +bool GLFWEventLoop::RunsTasksOnCurrentThread() const { + return std::this_thread::get_id() == main_thread_id_; +} + +void GLFWEventLoop::WaitForEvents(std::chrono::nanoseconds max_wait) { + const auto now = TaskTimePoint::clock::now(); + std::vector expired_tasks; + + // Process expired tasks. + { + std::lock_guard lock(task_queue_mutex_); + while (!task_queue_.empty()) { + const auto& top = task_queue_.top(); + // If this task (and all tasks after this) has not yet expired, there is + // nothing more to do. Quit iterating. + if (top.fire_time > now) { + break; + } + + // Make a record of the expired task. Do NOT service the task here + // because we are still holding onto the task queue mutex. We don't want + // other threads to block on posting tasks onto this thread till we are + // done processing expired tasks. + expired_tasks.push_back(task_queue_.top().task); + + // Remove the tasks from the delayed tasks queue. + task_queue_.pop(); + } + } + + // Fire expired tasks. + { + // Flushing tasks here without holing onto the task queue mutex. + for (const auto& task : expired_tasks) { + on_task_expired_(&task); + } + } + + // Sleep till the next task needs to be processed. If a new task comes + // along, the wait in GLFW will be resolved early because PostTask posts an + // empty event. + { + // Make sure the seconds are not integral. + using Seconds = std::chrono::duration>; + + std::lock_guard lock(task_queue_mutex_); + const auto next_wake = task_queue_.empty() ? TaskTimePoint::max() + : task_queue_.top().fire_time; + + const auto duration_to_wait = std::chrono::duration_cast( + std::min(next_wake - now, max_wait)); + + if (duration_to_wait.count() > 0.0) { + ::glfwWaitEventsTimeout(duration_to_wait.count()); + } else { + // Avoid engine task priority inversion by making sure GLFW events are + // always processed even when there is no need to wait for pending engine + // tasks. + ::glfwPollEvents(); + } + } +} + +GLFWEventLoop::TaskTimePoint GLFWEventLoop::TimePointFromFlutterTime( + uint64_t flutter_target_time_nanos) { + const auto now = TaskTimePoint::clock::now(); + const auto flutter_duration = + flutter_target_time_nanos - FlutterEngineGetCurrentTime(); + return now + std::chrono::nanoseconds(flutter_duration); +} + +void GLFWEventLoop::PostTask(FlutterTask flutter_task, + uint64_t flutter_target_time_nanos) { + static std::atomic_uint64_t sGlobalTaskOrder(0); + + Task task; + task.order = ++sGlobalTaskOrder; + task.fire_time = TimePointFromFlutterTime(flutter_target_time_nanos); + task.task = flutter_task; + + { + std::lock_guard lock(task_queue_mutex_); + task_queue_.push(task); + + // Make sure the queue mutex is unlocked before waking up the loop. In case + // the wake causes this thread to be descheduled for the primary thread to + // process tasks, the acquisition of the lock on that thread while holding + // the lock here momentarily till the end of the scope is a pessimization. + } + + ::glfwPostEmptyEvent(); +} + +} // namespace flutter diff --git a/shell/platform/glfw/glfw_event_loop.h b/shell/platform/glfw/glfw_event_loop.h new file mode 100644 index 0000000000000000000000000000000000000000..4c9185a60fc3dc744ae8d25f573c4235234466dd --- /dev/null +++ b/shell/platform/glfw/glfw_event_loop.h @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_GLFW_GLFW_EVENT_LOOP_H_ +#define FLUTTER_SHELL_PLATFORM_GLFW_GLFW_EVENT_LOOP_H_ + +#include +#include +#include +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" + +namespace flutter { + +// An event loop implementation that supports Flutter Engine tasks scheduling in +// the GLFW event loop. +class GLFWEventLoop { + public: + using TaskExpiredCallback = std::function; + GLFWEventLoop(std::thread::id main_thread_id, + TaskExpiredCallback on_task_expired); + + ~GLFWEventLoop(); + + // Returns if the current thread is the thread used by the GLFW event loop. + bool RunsTasksOnCurrentThread() const; + + // Wait for an any GLFW or pending Flutter Engine events and returns when + // either is encountered. Expired engine events are processed. The optional + // timeout should only be used when non-GLFW or engine events need to be + // processed in a polling manner. + void WaitForEvents( + std::chrono::nanoseconds max_wait = std::chrono::nanoseconds::max()); + + // Post a Flutter engine tasks to the event loop for delayed execution. + void PostTask(FlutterTask flutter_task, uint64_t flutter_target_time_nanos); + + private: + using TaskTimePoint = std::chrono::steady_clock::time_point; + struct Task { + uint64_t order; + TaskTimePoint fire_time; + FlutterTask task; + + struct Comparer { + bool operator()(const Task& a, const Task& b) { + if (a.fire_time == b.fire_time) { + return a.order > b.order; + } + return a.fire_time > b.fire_time; + } + }; + }; + std::thread::id main_thread_id_; + TaskExpiredCallback on_task_expired_; + std::mutex task_queue_mutex_; + std::priority_queue, Task::Comparer> task_queue_; + std::condition_variable task_queue_cv_; + + GLFWEventLoop(const GLFWEventLoop&) = delete; + + GLFWEventLoop& operator=(const GLFWEventLoop&) = delete; + + static TaskTimePoint TimePointFromFlutterTime( + uint64_t flutter_target_time_nanos); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_GLFW_GLFW_EVENT_LOOP_H_ diff --git a/tools/gn b/tools/gn index fe1ffb8d85ec686b7cf310c3dc740739a9d84357..66413f44fcfe92eac95d54226cf5207dd9deccd4 100755 --- a/tools/gn +++ b/tools/gn @@ -253,6 +253,7 @@ def to_gn_args(args): if sys.platform == 'darwin': gn_args['mac_sdk_path'] = args.mac_sdk_path + gn_args['build_glfw_shell'] = args.build_glfw_shell if gn_args['mac_sdk_path'] == '': gn_args['mac_sdk_path'] = os.getenv('FLUTTER_MAC_SDK_PATH', '') @@ -323,6 +324,9 @@ def parse_args(args): help='The IDE files to generate using GN. Use `gn gen help` and look for the --ide flag to' + ' see supported IDEs. If this flag is not specified, a platform specific default is selected.') + parser.add_argument('--build-glfw-shell', dest='build_glfw_shell', default=False, action='store_true', + help='Force building the GLFW shell on desktop platforms where it is not built by default.') + return parser.parse_args(args) def main(argv):