Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Wire up custom event loop interop for the GLFW embedder. #9089

Merged
merged 1 commit into from
Jun 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions shell/platform/glfw/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unrelated to the PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary after --build-glfw-shell on Mac.

libs = [
"CoreVideo.framework",
"IOKit.framework",
]
}
}

Expand Down
67 changes: 53 additions & 14 deletions shell/platform/glfw/flutter_glfw.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -79,8 +80,11 @@ struct FlutterDesktopWindowControllerState {
// Handler for the flutter/platform channel.
std::unique_ptr<flutter::PlatformHandler> 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<flutter::GLFWEventLoop> 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -528,6 +534,7 @@ static FlutterEngine RunFlutterEngine(GLFWwindow* window,
args.command_line_argc = static_cast<int>(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);
Expand Down Expand Up @@ -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<flutter::GLFWEventLoop>(
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<FlutterDesktopWindowControllerState*>(state)
->event_loop->RunsTasksOnCurrentThread();
};
platform_task_runner.post_task_callback =
[](FlutterTask task, uint64_t target_time_nanos, void* state) -> void {
reinterpret_cast<FlutterDesktopWindowControllerState*>(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;
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}
Expand Down
114 changes: 114 additions & 0 deletions shell/platform/glfw/glfw_event_loop.cc
Original file line number Diff line number Diff line change
@@ -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 <GLFW/glfw3.h>

#include <atomic>
#include <utility>

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<FlutterTask> expired_tasks;

// Process expired tasks.
{
std::lock_guard<std::mutex> 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.
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no scoped lock; why is there a scope block? (I would argue that the fact that you did this despite in not being any mechanical reason I can see is an indication that it should be broken into helpers.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. I wrote the comments first as pseudocode then filled it out with real code. As noted earlier, I don't think adding scopes means that the block should necessarily be extracted into its own helper routine.

// 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<double, std::ratio<1>>;

std::lock_guard<std::mutex> 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<Seconds>(
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<std::mutex> 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
73 changes: 73 additions & 0 deletions shell/platform/glfw/glfw_event_loop.h
Original file line number Diff line number Diff line change
@@ -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 <chrono>
#include <deque>
#include <mutex>
#include <queue>
#include <thread>

#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<void(const FlutterTask*)>;
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, std::deque<Task>, 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_
4 changes: 4 additions & 0 deletions tools/gn
Original file line number Diff line number Diff line change
Expand Up @@ -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', '')

Expand Down Expand Up @@ -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):
Expand Down