From 92fdf29e6f290b3a3aac696c4e9560b27c402aae Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 11 Mar 2025 10:30:03 +0100 Subject: [PATCH] [Linux] Move rendering to raster thread (#161879) Fixes https://github.com/flutter/flutter/issues/155045 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Robert Ancell --- .../flutter/shell/platform/linux/fl_engine.cc | 3 +- .../shell/platform/linux/fl_renderer.cc | 282 +++++++++++------- .../shell/platform/linux/fl_renderer_test.cc | 218 +++++++++++--- .../shell/platform/linux/fl_task_runner.cc | 37 ++- .../shell/platform/linux/fl_task_runner.h | 22 +- .../flutter/shell/platform/linux/fl_view.cc | 4 + 6 files changed, 410 insertions(+), 156 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 38edff1af9..108539afd9 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -356,7 +356,7 @@ static void fl_engine_post_task(FlutterTask task, void* user_data) { FlEngine* self = static_cast(user_data); - fl_task_runner_post_task(self->task_runner, task, target_time_nanos); + fl_task_runner_post_flutter_task(self->task_runner, task, target_time_nanos); } // Called when a platform message is received from the engine. @@ -630,7 +630,6 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { FlutterCustomTaskRunners custom_task_runners = {}; custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); custom_task_runners.platform_task_runner = &platform_task_runner; - custom_task_runners.render_task_runner = &platform_task_runner; g_autoptr(GPtrArray) command_line_args = g_ptr_array_new_with_free_func(g_free); diff --git a/engine/src/flutter/shell/platform/linux/fl_renderer.cc b/engine/src/flutter/shell/platform/linux/fl_renderer.cc index 2d5d918d0d..518b0b2904 100644 --- a/engine/src/flutter/shell/platform/linux/fl_renderer.cc +++ b/engine/src/flutter/shell/platform/linux/fl_renderer.cc @@ -73,6 +73,14 @@ typedef struct { // Framebuffers to render keyed by view ID. GHashTable* framebuffers_by_view_id; + + // Mutex used when blocking the raster thread until a task is completed on + // platform thread. + GMutex present_mutex; + + // Condition to unblock the raster thread after task is completed on platform + // thread. + GCond present_condition; } FlRendererPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FlRenderer, fl_renderer, G_TYPE_OBJECT) @@ -301,6 +309,151 @@ static void render(FlRenderer* self, } } +static gboolean present_layers(FlRenderer* self, + FlutterViewId view_id, + const FlutterLayer** layers, + size_t layers_count) { + FlRendererPrivate* priv = reinterpret_cast( + fl_renderer_get_instance_private(self)); + + g_return_val_if_fail(FL_IS_RENDERER(self), FALSE); + + // ignore incoming frame with wrong dimensions in trivial case with just one + // layer + if (priv->blocking_main_thread && layers_count == 1 && + layers[0]->offset.x == 0 && layers[0]->offset.y == 0 && + (layers[0]->size.width != priv->target_width || + layers[0]->size.height != priv->target_height)) { + return TRUE; + } + + priv->had_first_frame = true; + + fl_renderer_unblock_main_thread(self); + + g_autoptr(GPtrArray) framebuffers = + g_ptr_array_new_with_free_func(g_object_unref); + for (size_t i = 0; i < layers_count; ++i) { + const FlutterLayer* layer = layers[i]; + switch (layer->type) { + case kFlutterLayerContentTypeBackingStore: { + const FlutterBackingStore* backing_store = layer->backing_store; + FlFramebuffer* framebuffer = + FL_FRAMEBUFFER(backing_store->open_gl.framebuffer.user_data); + g_ptr_array_add(framebuffers, g_object_ref(framebuffer)); + } break; + case kFlutterLayerContentTypePlatformView: { + // TODO(robert-ancell) Not implemented - + // https://github.com/flutter/flutter/issues/41724 + } break; + } + } + + GWeakRef* ref = static_cast( + g_hash_table_lookup(priv->views, GINT_TO_POINTER(view_id))); + g_autoptr(FlRenderable) renderable = + ref != nullptr ? FL_RENDERABLE(g_weak_ref_get(ref)) : nullptr; + if (renderable == nullptr) { + return TRUE; + } + + if (view_id == flutter::kFlutterImplicitViewId) { + // Store for rendering later + g_hash_table_insert(priv->framebuffers_by_view_id, GINT_TO_POINTER(view_id), + g_ptr_array_ref(framebuffers)); + } else { + // Composite into a single framebuffer. + if (framebuffers->len > 1) { + size_t width = 0, height = 0; + + for (guint i = 0; i < framebuffers->len; i++) { + FlFramebuffer* framebuffer = + FL_FRAMEBUFFER(g_ptr_array_index(framebuffers, i)); + + size_t w = fl_framebuffer_get_width(framebuffer); + size_t h = fl_framebuffer_get_height(framebuffer); + if (w > width) { + width = w; + } + if (h > height) { + height = h; + } + } + + FlFramebuffer* view_framebuffer = + fl_framebuffer_new(priv->general_format, width, height); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, + fl_framebuffer_get_id(view_framebuffer)); + render(self, framebuffers, width, height); + g_ptr_array_set_size(framebuffers, 0); + g_ptr_array_add(framebuffers, view_framebuffer); + } + + // Read back pixel values. + FlFramebuffer* framebuffer = + FL_FRAMEBUFFER(g_ptr_array_index(framebuffers, 0)); + size_t width = fl_framebuffer_get_width(framebuffer); + size_t height = fl_framebuffer_get_height(framebuffer); + size_t data_length = width * height * 4; + g_autofree uint8_t* data = static_cast(malloc(data_length)); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fl_framebuffer_get_id(framebuffer)); + glReadPixels(0, 0, width, height, priv->general_format, GL_UNSIGNED_BYTE, + data); + + // Write into a texture in the views context. + fl_renderable_make_current(renderable); + FlFramebuffer* view_framebuffer = + fl_framebuffer_new(priv->general_format, width, height); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, + fl_framebuffer_get_id(view_framebuffer)); + glBindTexture(GL_TEXTURE_2D, + fl_framebuffer_get_texture_id(view_framebuffer)); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, + GL_UNSIGNED_BYTE, data); + + g_autoptr(GPtrArray) secondary_framebuffers = + g_ptr_array_new_with_free_func(g_object_unref); + g_ptr_array_add(secondary_framebuffers, g_object_ref(view_framebuffer)); + g_hash_table_insert(priv->framebuffers_by_view_id, GINT_TO_POINTER(view_id), + g_ptr_array_ref(secondary_framebuffers)); + } + + fl_renderable_redraw(renderable); + + return TRUE; +} + +typedef struct { + FlRenderer* self; + + FlutterViewId view_id; + + const FlutterLayer** layers; + size_t layers_count; + + gboolean result; + + gboolean finished; +} PresentLayersData; + +// Perform the present on the main thread. +static void present_layers_task_cb(gpointer user_data) { + PresentLayersData* data = static_cast(user_data); + FlRendererPrivate* priv = reinterpret_cast( + fl_renderer_get_instance_private(data->self)); + + // Perform the present. + fl_renderer_make_current(data->self); + data->result = present_layers(data->self, data->view_id, data->layers, + data->layers_count); + fl_renderer_clear_current(data->self); + + // Complete fl_renderer_present_layers(). + g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->present_mutex); + data->finished = TRUE; + g_cond_signal(&priv->present_condition); +} + static void fl_renderer_dispose(GObject* object) { FlRenderer* self = FL_RENDERER(object); FlRendererPrivate* priv = reinterpret_cast( @@ -311,6 +464,8 @@ static void fl_renderer_dispose(GObject* object) { g_weak_ref_clear(&priv->engine); g_clear_pointer(&priv->views, g_hash_table_unref); g_clear_pointer(&priv->framebuffers_by_view_id, g_hash_table_unref); + g_mutex_clear(&priv->present_mutex); + g_cond_clear(&priv->present_condition); G_OBJECT_CLASS(fl_renderer_parent_class)->dispose(object); } @@ -327,6 +482,8 @@ static void fl_renderer_init(FlRenderer* self) { priv->framebuffers_by_view_id = g_hash_table_new_full( g_direct_hash, g_direct_equal, nullptr, reinterpret_cast(g_ptr_array_unref)); + g_mutex_init(&priv->present_mutex); + g_cond_init(&priv->present_condition); } void fl_renderer_set_engine(FlRenderer* self, FlEngine* engine) { @@ -454,114 +611,37 @@ gboolean fl_renderer_present_layers(FlRenderer* self, FlutterViewId view_id, const FlutterLayer** layers, size_t layers_count) { + // Detach the context from raster thread. Needed because blitting + // will be done on the main thread, which will make the context current. + fl_renderer_clear_current(self); + FlRendererPrivate* priv = reinterpret_cast( fl_renderer_get_instance_private(self)); + g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&priv->engine)); - g_return_val_if_fail(FL_IS_RENDERER(self), FALSE); + // Schedule the present to run on the main thread. + FlTaskRunner* task_runner = fl_engine_get_task_runner(engine); + PresentLayersData data = { + .self = self, + .view_id = view_id, + .layers = layers, + .layers_count = layers_count, + .result = FALSE, + .finished = FALSE, + }; + fl_task_runner_post_callback(task_runner, present_layers_task_cb, &data); - // ignore incoming frame with wrong dimensions in trivial case with just one - // layer - if (priv->blocking_main_thread && layers_count == 1 && - layers[0]->offset.x == 0 && layers[0]->offset.y == 0 && - (layers[0]->size.width != priv->target_width || - layers[0]->size.height != priv->target_height)) { - return TRUE; + // Block until present completes. + g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->present_mutex); + while (!data.finished) { + g_cond_wait(&priv->present_condition, &priv->present_mutex); } - priv->had_first_frame = true; + // Restore the context to the raster thread in case the engine needs it + // to do some cleanup. + fl_renderer_make_current(self); - fl_renderer_unblock_main_thread(self); - - g_autoptr(GPtrArray) framebuffers = - g_ptr_array_new_with_free_func(g_object_unref); - for (size_t i = 0; i < layers_count; ++i) { - const FlutterLayer* layer = layers[i]; - switch (layer->type) { - case kFlutterLayerContentTypeBackingStore: { - const FlutterBackingStore* backing_store = layer->backing_store; - FlFramebuffer* framebuffer = - FL_FRAMEBUFFER(backing_store->open_gl.framebuffer.user_data); - g_ptr_array_add(framebuffers, g_object_ref(framebuffer)); - } break; - case kFlutterLayerContentTypePlatformView: { - // TODO(robert-ancell) Not implemented - - // https://github.com/flutter/flutter/issues/41724 - } break; - } - } - - GWeakRef* ref = static_cast( - g_hash_table_lookup(priv->views, GINT_TO_POINTER(view_id))); - g_autoptr(FlRenderable) renderable = - ref != nullptr ? FL_RENDERABLE(g_weak_ref_get(ref)) : nullptr; - if (renderable == nullptr) { - return TRUE; - } - - if (view_id == flutter::kFlutterImplicitViewId) { - // Store for rendering later - g_hash_table_insert(priv->framebuffers_by_view_id, GINT_TO_POINTER(view_id), - g_ptr_array_ref(framebuffers)); - } else { - // Composite into a single framebuffer. - if (framebuffers->len > 1) { - size_t width = 0, height = 0; - - for (guint i = 0; i < framebuffers->len; i++) { - FlFramebuffer* framebuffer = - FL_FRAMEBUFFER(g_ptr_array_index(framebuffers, i)); - - size_t w = fl_framebuffer_get_width(framebuffer); - size_t h = fl_framebuffer_get_height(framebuffer); - if (w > width) { - width = w; - } - if (h > height) { - height = h; - } - } - - FlFramebuffer* view_framebuffer = - fl_framebuffer_new(priv->general_format, width, height); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, - fl_framebuffer_get_id(view_framebuffer)); - render(self, framebuffers, width, height); - g_ptr_array_set_size(framebuffers, 0); - g_ptr_array_add(framebuffers, view_framebuffer); - } - - // Read back pixel values. - FlFramebuffer* framebuffer = - FL_FRAMEBUFFER(g_ptr_array_index(framebuffers, 0)); - size_t width = fl_framebuffer_get_width(framebuffer); - size_t height = fl_framebuffer_get_height(framebuffer); - size_t data_length = width * height * 4; - g_autofree uint8_t* data = static_cast(malloc(data_length)); - glBindFramebuffer(GL_READ_FRAMEBUFFER, fl_framebuffer_get_id(framebuffer)); - glReadPixels(0, 0, width, height, priv->general_format, GL_UNSIGNED_BYTE, - data); - - // Write into a texture in the views context. - fl_renderable_make_current(renderable); - FlFramebuffer* view_framebuffer = - fl_framebuffer_new(priv->general_format, width, height); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, - fl_framebuffer_get_id(view_framebuffer)); - glBindTexture(GL_TEXTURE_2D, - fl_framebuffer_get_texture_id(view_framebuffer)); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, - GL_UNSIGNED_BYTE, data); - - g_autoptr(GPtrArray) secondary_framebuffers = - g_ptr_array_new_with_free_func(g_object_unref); - g_ptr_array_add(secondary_framebuffers, g_object_ref(view_framebuffer)); - g_hash_table_insert(priv->framebuffers_by_view_id, GINT_TO_POINTER(view_id), - g_ptr_array_ref(secondary_framebuffers)); - } - - fl_renderable_redraw(renderable); - - return TRUE; + return data.result; } void fl_renderer_setup(FlRenderer* self) { diff --git a/engine/src/flutter/shell/platform/linux/fl_renderer_test.cc b/engine/src/flutter/shell/platform/linux/fl_renderer_test.cc index 0c808fb2f1..ab300fad0d 100644 --- a/engine/src/flutter/shell/platform/linux/fl_renderer_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_renderer_test.cc @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include #include "gtest/gtest.h" #include "flutter/common/constants.h" #include "flutter/fml/logging.h" +#include "flutter/fml/synchronization/waitable_event.h" #include "flutter/shell/platform/linux/fl_framebuffer.h" #include "flutter/shell/platform/linux/testing/mock_epoxy.h" #include "flutter/shell/platform/linux/testing/mock_renderer.h" @@ -14,6 +16,8 @@ TEST(FlRendererTest, BackgroundColor) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); ON_CALL(epoxy, epoxy_is_desktop_gl).WillByDefault(::testing::Return(true)); EXPECT_CALL(epoxy, epoxy_gl_version).WillRepeatedly(::testing::Return(30)); @@ -25,6 +29,7 @@ TEST(FlRendererTest, BackgroundColor) { g_autoptr(FlMockRenderable) renderable = fl_mock_renderable_new(); g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); fl_renderer_setup(FL_RENDERER(renderer)); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); fl_renderer_add_renderable(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, FL_RENDERABLE(renderable)); @@ -35,27 +40,47 @@ TEST(FlRendererTest, BackgroundColor) { FlutterBackingStore backing_store; fl_renderer_create_backing_store(FL_RENDERER(renderer), &config, &backing_store); - const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), - .type = kFlutterLayerContentTypeBackingStore, - .backing_store = &backing_store, - .size = {.width = 1024, .height = 1024}}; - const FlutterLayer* layers[] = {&layer0}; - fl_renderer_present_layers(FL_RENDERER(renderer), - flutter::kFlutterImplicitViewId, layers, 1); + + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .size = {.width = 1024, .height = 1024}}; + const FlutterLayer* layers[] = {&layer0}; + + fl_renderer_present_layers(FL_RENDERER(renderer), + flutter::kFlutterImplicitViewId, layers, 1); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(renderable) == 0) { + g_main_context_iteration(nullptr, true); + } + GdkRGBA background_color = { .red = 0.2, .green = 0.3, .blue = 0.4, .alpha = 0.5}; fl_renderer_render(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, 1024, 1024, &background_color); + + // Wait until the raster thread has finished before letting + // the engine go out of scope. + latch.Wait(); } TEST(FlRendererTest, RestoresGLState) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); constexpr int kWidth = 100; constexpr int kHeight = 100; g_autoptr(FlMockRenderable) renderable = fl_mock_renderable_new(); g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); g_autoptr(FlFramebuffer) framebuffer = fl_framebuffer_new(GL_RGB, kWidth, kHeight); @@ -79,9 +104,20 @@ TEST(FlRendererTest, RestoresGLState) { constexpr GLuint kFakeTextureName = 123; glBindTexture(GL_TEXTURE_2D, kFakeTextureName); - fl_renderer_present_layers(FL_RENDERER(renderer), - flutter::kFlutterImplicitViewId, layers.data(), - layers.size()); + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + fl_renderer_present_layers(FL_RENDERER(renderer), + flutter::kFlutterImplicitViewId, layers.data(), + layers.size()); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(renderable) == 0) { + g_main_context_iteration(nullptr, true); + } + GdkRGBA background_color = { .red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}; fl_renderer_render(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, @@ -91,10 +127,16 @@ TEST(FlRendererTest, RestoresGLState) { glGetIntegerv(GL_TEXTURE_BINDING_2D, reinterpret_cast(&texture_2d_binding)); EXPECT_EQ(texture_2d_binding, kFakeTextureName); + + // Wait until the raster thread has finished before letting + // the engine go out of scope. + latch.Wait(); } TEST(FlRendererTest, BlitFramebuffer) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); // OpenGL 3.0 ON_CALL(epoxy, glGetString(GL_VENDOR)) @@ -108,6 +150,7 @@ TEST(FlRendererTest, BlitFramebuffer) { g_autoptr(FlMockRenderable) renderable = fl_mock_renderable_new(); g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); fl_renderer_setup(FL_RENDERER(renderer)); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); fl_renderer_add_renderable(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, FL_RENDERABLE(renderable)); @@ -118,21 +161,37 @@ TEST(FlRendererTest, BlitFramebuffer) { FlutterBackingStore backing_store; fl_renderer_create_backing_store(FL_RENDERER(renderer), &config, &backing_store); - const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), - .type = kFlutterLayerContentTypeBackingStore, - .backing_store = &backing_store, - .size = {.width = 1024, .height = 1024}}; - const FlutterLayer* layers[] = {&layer0}; - fl_renderer_present_layers(FL_RENDERER(renderer), - flutter::kFlutterImplicitViewId, layers, 1); + + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .size = {.width = 1024, .height = 1024}}; + const FlutterLayer* layers[] = {&layer0}; + fl_renderer_present_layers(FL_RENDERER(renderer), + flutter::kFlutterImplicitViewId, layers, 1); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(renderable) == 0) { + g_main_context_iteration(nullptr, true); + } + GdkRGBA background_color = { .red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}; fl_renderer_render(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, 1024, 1024, &background_color); + + latch.Wait(); } TEST(FlRendererTest, BlitFramebufferExtension) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); // OpenGL 2.0 with GL_EXT_framebuffer_blit extension ON_CALL(epoxy, glGetString(GL_VENDOR)) @@ -151,6 +210,7 @@ TEST(FlRendererTest, BlitFramebufferExtension) { g_autoptr(FlMockRenderable) renderable = fl_mock_renderable_new(); g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); fl_renderer_setup(FL_RENDERER(renderer)); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); fl_renderer_add_renderable(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, FL_RENDERABLE(renderable)); @@ -161,21 +221,37 @@ TEST(FlRendererTest, BlitFramebufferExtension) { FlutterBackingStore backing_store; fl_renderer_create_backing_store(FL_RENDERER(renderer), &config, &backing_store); - const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), - .type = kFlutterLayerContentTypeBackingStore, - .backing_store = &backing_store, - .size = {.width = 1024, .height = 1024}}; - const FlutterLayer* layers[] = {&layer0}; - fl_renderer_present_layers(FL_RENDERER(renderer), - flutter::kFlutterImplicitViewId, layers, 1); + + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .size = {.width = 1024, .height = 1024}}; + const FlutterLayer* layers[] = {&layer0}; + fl_renderer_present_layers(FL_RENDERER(renderer), + flutter::kFlutterImplicitViewId, layers, 1); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(renderable) == 0) { + g_main_context_iteration(nullptr, true); + } GdkRGBA background_color = { .red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}; fl_renderer_render(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, 1024, 1024, &background_color); + // Wait until the raster thread has finished before letting + // the engine go out of scope. + latch.Wait(); } TEST(FlRendererTest, NoBlitFramebuffer) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); // OpenGL 2.0 ON_CALL(epoxy, glGetString(GL_VENDOR)) @@ -187,6 +263,7 @@ TEST(FlRendererTest, NoBlitFramebuffer) { g_autoptr(FlMockRenderable) renderable = fl_mock_renderable_new(); g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); fl_renderer_setup(FL_RENDERER(renderer)); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); fl_renderer_add_renderable(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, FL_RENDERABLE(renderable)); @@ -197,21 +274,39 @@ TEST(FlRendererTest, NoBlitFramebuffer) { FlutterBackingStore backing_store; fl_renderer_create_backing_store(FL_RENDERER(renderer), &config, &backing_store); - const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), - .type = kFlutterLayerContentTypeBackingStore, - .backing_store = &backing_store, - .size = {.width = 1024, .height = 1024}}; - const FlutterLayer* layers[] = {&layer0}; - fl_renderer_present_layers(FL_RENDERER(renderer), - flutter::kFlutterImplicitViewId, layers, 1); + + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .size = {.width = 1024, .height = 1024}}; + const FlutterLayer* layers[] = {&layer0}; + fl_renderer_present_layers(FL_RENDERER(renderer), + flutter::kFlutterImplicitViewId, layers, 1); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(renderable) == 0) { + g_main_context_iteration(nullptr, true); + } + GdkRGBA background_color = { .red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}; fl_renderer_render(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, 1024, 1024, &background_color); + + // Wait until the raster thread has finished before letting + // the engine go out of scope. + latch.Wait(); } TEST(FlRendererTest, BlitFramebufferNvidia) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); // OpenGL 3.0, but on NVIDIA driver so temporarily disabled due to // https://github.com/flutter/flutter/issues/152099 @@ -224,6 +319,7 @@ TEST(FlRendererTest, BlitFramebufferNvidia) { g_autoptr(FlMockRenderable) renderable = fl_mock_renderable_new(); g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); fl_renderer_setup(FL_RENDERER(renderer)); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); fl_renderer_add_renderable(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, FL_RENDERABLE(renderable)); @@ -234,21 +330,39 @@ TEST(FlRendererTest, BlitFramebufferNvidia) { FlutterBackingStore backing_store; fl_renderer_create_backing_store(FL_RENDERER(renderer), &config, &backing_store); - const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), - .type = kFlutterLayerContentTypeBackingStore, - .backing_store = &backing_store, - .size = {.width = 1024, .height = 1024}}; - const FlutterLayer* layers[] = {&layer0}; - fl_renderer_present_layers(FL_RENDERER(renderer), - flutter::kFlutterImplicitViewId, layers, 1); + + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .size = {.width = 1024, .height = 1024}}; + const FlutterLayer* layers[] = {&layer0}; + fl_renderer_present_layers(FL_RENDERER(renderer), + flutter::kFlutterImplicitViewId, layers, 1); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(renderable) == 0) { + g_main_context_iteration(nullptr, true); + } + GdkRGBA background_color = { .red = 0.0, .green = 0.0, .blue = 0.0, .alpha = 1.0}; fl_renderer_render(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, 1024, 1024, &background_color); + + // Wait until the raster thread has finished before letting + // the engine go out of scope. + latch.Wait(); } TEST(FlRendererTest, MultiView) { ::testing::NiceMock epoxy; + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); // OpenGL 3.0 ON_CALL(epoxy, glGetString(GL_VENDOR)) @@ -262,6 +376,7 @@ TEST(FlRendererTest, MultiView) { g_autoptr(FlMockRenderer) renderer = fl_mock_renderer_new(); fl_renderer_setup(FL_RENDERER(renderer)); + fl_renderer_set_engine(FL_RENDERER(renderer), engine); fl_renderer_add_renderable(FL_RENDERER(renderer), flutter::kFlutterImplicitViewId, FL_RENDERABLE(renderable)); @@ -280,15 +395,30 @@ TEST(FlRendererTest, MultiView) { FlutterBackingStore backing_store; fl_renderer_create_backing_store(FL_RENDERER(renderer), &config, &backing_store); - const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), - .type = kFlutterLayerContentTypeBackingStore, - .backing_store = &backing_store, - .size = {.width = 1024, .height = 1024}}; - const FlutterLayer* layers[] = {&layer0}; - fl_renderer_present_layers(FL_RENDERER(renderer), 1, layers, 1); + + fml::AutoResetWaitableEvent latch; + + // Simulate raster thread. + std::thread([&]() { + const FlutterLayer layer0 = {.struct_size = sizeof(FlutterLayer), + .type = kFlutterLayerContentTypeBackingStore, + .backing_store = &backing_store, + .size = {.width = 1024, .height = 1024}}; + const FlutterLayer* layers[] = {&layer0}; + fl_renderer_present_layers(FL_RENDERER(renderer), 1, layers, 1); + latch.Signal(); + }).detach(); + + while (fl_mock_renderable_get_redraw_count(secondary_renderable) == 0) { + g_main_context_iteration(nullptr, true); + } EXPECT_EQ(fl_mock_renderable_get_redraw_count(renderable), static_cast(0)); EXPECT_EQ(fl_mock_renderable_get_redraw_count(secondary_renderable), static_cast(1)); + + // Wait until the raster thread has finished before letting + // the engine go out of scope. + latch.Wait(); } diff --git a/engine/src/flutter/shell/platform/linux/fl_task_runner.cc b/engine/src/flutter/shell/platform/linux/fl_task_runner.cc index 147550c504..5a225da1ed 100644 --- a/engine/src/flutter/shell/platform/linux/fl_task_runner.cc +++ b/engine/src/flutter/shell/platform/linux/fl_task_runner.cc @@ -22,9 +22,18 @@ struct _FlTaskRunner { }; typedef struct _FlTaskRunnerTask { - // absolute time of task (based on g_get_monotonic_time) + // absolute time of task (based on g_get_monotonic_time). gint64 task_time_micros; + + // flutter task to execute if schedule through + // fl_task_runner_post_flutter_task. FlutterTask task; + + // callback to execute if scheduled through fl_task_runner_post_callback. + void (*callback)(gpointer data); + + // data for the callback. + gpointer callback_data; } FlTaskRunnerTask; G_DEFINE_TYPE(FlTaskRunner, fl_task_runner, G_TYPE_OBJECT) @@ -56,7 +65,11 @@ static void fl_task_runner_process_expired_tasks_locked(FlTaskRunner* self) { l = expired_tasks; while (l != nullptr) { FlTaskRunnerTask* task = static_cast(l->data); - fl_engine_execute_task(engine, &task->task); + if (task->callback != nullptr) { + task->callback(task->callback_data); + } else { + fl_engine_execute_task(engine, &task->task); + } l = l->next; } } @@ -158,9 +171,9 @@ FlTaskRunner* fl_task_runner_new(FlEngine* engine) { return self; } -void fl_task_runner_post_task(FlTaskRunner* self, - FlutterTask task, - uint64_t target_time_nanos) { +void fl_task_runner_post_flutter_task(FlTaskRunner* self, + FlutterTask task, + uint64_t target_time_nanos) { g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex); (void)locker; // unused variable @@ -173,6 +186,20 @@ void fl_task_runner_post_task(FlTaskRunner* self, fl_task_runner_tasks_did_change_locked(self); } +void fl_task_runner_post_callback(FlTaskRunner* self, + void (*callback)(gpointer data), + gpointer data) { + g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex); + (void)locker; // unused variable + + FlTaskRunnerTask* runner_task = g_new0(FlTaskRunnerTask, 1); + runner_task->callback = callback; + runner_task->callback_data = data; + + self->pending_tasks = g_list_append(self->pending_tasks, runner_task); + fl_task_runner_tasks_did_change_locked(self); +} + void fl_task_runner_block_main_thread(FlTaskRunner* self) { g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex); (void)locker; // unused variable diff --git a/engine/src/flutter/shell/platform/linux/fl_task_runner.h b/engine/src/flutter/shell/platform/linux/fl_task_runner.h index ca4f84b3ee..0c7c7effc2 100644 --- a/engine/src/flutter/shell/platform/linux/fl_task_runner.h +++ b/engine/src/flutter/shell/platform/linux/fl_task_runner.h @@ -25,7 +25,7 @@ G_DECLARE_FINAL_TYPE(FlTaskRunner, fl_task_runner, FL, TASK_RUNNER, GObject); FlTaskRunner* fl_task_runner_new(FlEngine* engine); /** - * fl_task_runner_post_task: + * fl_task_runner_post_flutter_task: * @task_runner: an #FlTaskRunner. * @task: Flutter task being scheduled * @target_time_nanos: absolute time in nanoseconds @@ -33,9 +33,23 @@ FlTaskRunner* fl_task_runner_new(FlEngine* engine); * Posts a Flutter task to be executed on main thread. This function is thread * safe and may be called from any thread. */ -void fl_task_runner_post_task(FlTaskRunner* task_runner, - FlutterTask task, - uint64_t target_time_nanos); +void fl_task_runner_post_flutter_task(FlTaskRunner* task_runner, + FlutterTask task, + uint64_t target_time_nanos); + +/** + * fl_task_runner_post_callback: + * @task_runner: an #FlTaskRunner. + * @callback: callback to be scheduled + * @data: data to be passed to the callback + * + * Schedules arbitrary callback to be executed on main thread. The callback + * will be executed in next run loop turn. This function is thread + * safe and may be called from any thread. + */ +void fl_task_runner_post_callback(FlTaskRunner* task_runner, + void (*callback)(gpointer data), + gpointer data); /** * fl_task_runner_block_main_thread: diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index c23aa67b92..83dcea8e6b 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -476,6 +476,10 @@ static void realize_cb(FlView* self) { fl_renderer_add_renderable(FL_RENDERER(self->renderer), self->view_id, FL_RENDERABLE(self)); + // Flutter engine will need to make the context current from raster thread + // during initialization. + fl_renderer_clear_current(FL_RENDERER(self->renderer)); + if (!fl_engine_start(self->engine, &error)) { g_warning("Failed to start Flutter engine: %s", error->message); return;