diff --git a/shell/common/shell_test_platform_view_gl.cc b/shell/common/shell_test_platform_view_gl.cc index 492354230e8dc..ac68ff3971d0b 100644 --- a/shell/common/shell_test_platform_view_gl.cc +++ b/shell/common/shell_test_platform_view_gl.cc @@ -69,8 +69,10 @@ bool ShellTestPlatformViewGL::GLContextPresent( } // |GPUSurfaceGLDelegate| -intptr_t ShellTestPlatformViewGL::GLContextFBO(GLFrameInfo frame_info) const { - return gl_surface_.GetFramebuffer(frame_info.width, frame_info.height); +GLFBOInfo ShellTestPlatformViewGL::GLContextFBO(GLFrameInfo frame_info) const { + return GLFBOInfo{ + .fbo_id = gl_surface_.GetFramebuffer(frame_info.width, frame_info.height), + }; } // |GPUSurfaceGLDelegate| diff --git a/shell/common/shell_test_platform_view_gl.h b/shell/common/shell_test_platform_view_gl.h index c890c1e0fc6ec..b4afc1266975b 100644 --- a/shell/common/shell_test_platform_view_gl.h +++ b/shell/common/shell_test_platform_view_gl.h @@ -61,7 +61,7 @@ class ShellTestPlatformViewGL : public ShellTestPlatformView, bool GLContextPresent(const GLPresentInfo& present_info) override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO(GLFrameInfo frame_info) const override; + GLFBOInfo GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| GLProcResolver GetGLProcResolver() const override; diff --git a/shell/gpu/gpu_surface_gl_delegate.h b/shell/gpu/gpu_surface_gl_delegate.h index f9da6a11120fd..d1e31b96559f5 100644 --- a/shell/gpu/gpu_surface_gl_delegate.h +++ b/shell/gpu/gpu_surface_gl_delegate.h @@ -22,17 +22,32 @@ struct GLFrameInfo { uint32_t height; }; +// A structure to represent the frame buffer information which is returned to +// the rendering backend after requesting a frame buffer object. +struct GLFBOInfo { + // The frame buffer's ID. + uint32_t fbo_id; + // This boolean flags whether the returned FBO supports partial repaint. + const bool partial_repaint_enabled; + // The frame buffer's existing damage (i.e. damage since it was last used). + const SkIRect existing_damage; +}; + // Information passed during presentation of a frame. struct GLPresentInfo { uint32_t fbo_id; - // Damage is a hint to compositor telling it which parts of front buffer - // need to be updated - const std::optional& damage; + // The frame damage is a hint to compositor telling it which parts of front + // buffer need to be updated. + const std::optional& frame_damage; // Time at which this frame is scheduled to be presented. This is a hint // that can be passed to the platform to drop queued frames. std::optional presentation_time = std::nullopt; + + // The buffer damage refers to the region that needs to be set as damaged + // within the frame buffer. + const std::optional& buffer_damage; }; class GPUSurfaceGLDelegate { @@ -54,8 +69,9 @@ class GPUSurfaceGLDelegate { // context and not any of the contexts dedicated for IO. virtual bool GLContextPresent(const GLPresentInfo& present_info) = 0; - // The ID of the main window bound framebuffer. Typically FBO0. - virtual intptr_t GLContextFBO(GLFrameInfo frame_info) const = 0; + // The information about the main window bound framebuffer. ID is Typically + // FBO0. + virtual GLFBOInfo GLContextFBO(GLFrameInfo frame_info) const = 0; // The rendering subsystem assumes that the ID of the main window bound // framebuffer remains constant throughout. If this assumption in incorrect, diff --git a/shell/gpu/gpu_surface_gl_impeller.cc b/shell/gpu/gpu_surface_gl_impeller.cc index 24413147274a6..3fc7c441b42bb 100644 --- a/shell/gpu/gpu_surface_gl_impeller.cc +++ b/shell/gpu/gpu_surface_gl_impeller.cc @@ -62,10 +62,11 @@ std::unique_ptr GPUSurfaceGLImpeller::AcquireFrame( if (weak) { GLPresentInfo present_info = { .fbo_id = 0, - .damage = std::nullopt, + .frame_damage = std::nullopt, // TODO (https://github.com/flutter/flutter/issues/105597): wire-up // presentation time to impeller backend. .presentation_time = std::nullopt, + .buffer_damage = std::nullopt, }; delegate->GLContextPresent(present_info); } diff --git a/shell/gpu/gpu_surface_gl_skia.cc b/shell/gpu/gpu_surface_gl_skia.cc index 66bb6636d95d8..eafb6120b3774 100644 --- a/shell/gpu/gpu_surface_gl_skia.cc +++ b/shell/gpu/gpu_surface_gl_skia.cc @@ -181,10 +181,10 @@ bool GPUSurfaceGLSkia::CreateOrUpdateSurfaces(const SkISize& size) { GLFrameInfo frame_info = {static_cast(size.width()), static_cast(size.height())}; - const uint32_t fbo_id = delegate_->GLContextFBO(frame_info); + const GLFBOInfo fbo_info = delegate_->GLContextFBO(frame_info); onscreen_surface = WrapOnscreenSurface(context_.get(), // GL context size, // root surface size - fbo_id // window FBO ID + fbo_info.fbo_id // window FBO ID ); if (onscreen_surface == nullptr) { @@ -195,7 +195,9 @@ bool GPUSurfaceGLSkia::CreateOrUpdateSurfaces(const SkISize& size) { } onscreen_surface_ = std::move(onscreen_surface); - fbo_id_ = fbo_id; + fbo_id_ = fbo_info.fbo_id; + supports_partial_repaint_ = fbo_info.partial_repaint_enabled; + existing_damage_ = fbo_info.existing_damage; return true; } @@ -248,6 +250,9 @@ std::unique_ptr GPUSurfaceGLSkia::AcquireFrame( }; framebuffer_info = delegate_->GLContextFramebufferInfo(); + // Partial repaint is enabled by default + framebuffer_info.supports_partial_repaint = supports_partial_repaint_; + framebuffer_info.existing_damage = existing_damage_; return std::make_unique(surface, std::move(framebuffer_info), submit_callback, std::move(context_switch)); @@ -268,8 +273,9 @@ bool GPUSurfaceGLSkia::PresentSurface(const SurfaceFrame& frame, GLPresentInfo present_info = { .fbo_id = fbo_id_, - .damage = frame.submit_info().frame_damage, + .frame_damage = frame.submit_info().frame_damage, .presentation_time = frame.submit_info().presentation_time, + .buffer_damage = frame.submit_info().buffer_damage, }; if (!delegate_->GLContextPresent(present_info)) { return false; @@ -284,11 +290,11 @@ bool GPUSurfaceGLSkia::PresentSurface(const SurfaceFrame& frame, // The FBO has changed, ask the delegate for the new FBO and do a surface // re-wrap. - const uint32_t fbo_id = delegate_->GLContextFBO(frame_info); + const GLFBOInfo fbo_info = delegate_->GLContextFBO(frame_info); auto new_onscreen_surface = WrapOnscreenSurface(context_.get(), // GL context current_size, // root surface size - fbo_id // window FBO ID + fbo_info.fbo_id // window FBO ID ); if (!new_onscreen_surface) { @@ -296,7 +302,9 @@ bool GPUSurfaceGLSkia::PresentSurface(const SurfaceFrame& frame, } onscreen_surface_ = std::move(new_onscreen_surface); - fbo_id_ = fbo_id; + fbo_id_ = fbo_info.fbo_id; + supports_partial_repaint_ = fbo_info.partial_repaint_enabled; + existing_damage_ = fbo_info.existing_damage; } return true; diff --git a/shell/gpu/gpu_surface_gl_skia.h b/shell/gpu/gpu_surface_gl_skia.h index 90cad241945fd..39f6e3f607dfe 100644 --- a/shell/gpu/gpu_surface_gl_skia.h +++ b/shell/gpu/gpu_surface_gl_skia.h @@ -67,6 +67,8 @@ class GPUSurfaceGLSkia : public Surface { sk_sp onscreen_surface_; /// FBO backing the current `onscreen_surface_`. uint32_t fbo_id_ = 0; + // Private variable used to keep track of the current FBO's existing damage. + SkIRect existing_damage_ = SkIRect::MakeEmpty(); bool context_owner_ = false; // TODO(38466): Refactor GPU surface APIs take into account the fact that an // external view embedder may want to render to the root surface. This is a @@ -74,6 +76,8 @@ class GPUSurfaceGLSkia : public Surface { // external view embedder is present. const bool render_to_surface_ = true; bool valid_ = false; + // Partial repaint is on by default. + bool supports_partial_repaint_ = true; // WeakPtrFactory must be the last member. fml::TaskRunnerAffineWeakPtrFactory weak_factory_; diff --git a/shell/platform/android/android_surface_gl_impeller.cc b/shell/platform/android/android_surface_gl_impeller.cc index 28c1cfe55f19e..635acc77a9502 100644 --- a/shell/platform/android/android_surface_gl_impeller.cc +++ b/shell/platform/android/android_surface_gl_impeller.cc @@ -295,9 +295,11 @@ bool AndroidSurfaceGLImpeller::GLContextPresent( } // |GPUSurfaceGLDelegate| -intptr_t AndroidSurfaceGLImpeller::GLContextFBO(GLFrameInfo frame_info) const { +GLFBOInfo AndroidSurfaceGLImpeller::GLContextFBO(GLFrameInfo frame_info) const { // FBO0 is the default window bound framebuffer in EGL environments. - return 0; + return GLFBOInfo{ + .fbo_id = 0, + }; } // |GPUSurfaceGLDelegate| diff --git a/shell/platform/android/android_surface_gl_impeller.h b/shell/platform/android/android_surface_gl_impeller.h index 8bcf14dbee27b..9d2399868da97 100644 --- a/shell/platform/android/android_surface_gl_impeller.h +++ b/shell/platform/android/android_surface_gl_impeller.h @@ -68,7 +68,7 @@ class AndroidSurfaceGLImpeller final : public GPUSurfaceGLDelegate, bool GLContextPresent(const GLPresentInfo& present_info) override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO(GLFrameInfo frame_info) const override; + GLFBOInfo GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| sk_sp GetGLInterface() const override; diff --git a/shell/platform/android/android_surface_gl_skia.cc b/shell/platform/android/android_surface_gl_skia.cc index 23242c9b3bad4..50ac30835bb1f 100644 --- a/shell/platform/android/android_surface_gl_skia.cc +++ b/shell/platform/android/android_surface_gl_skia.cc @@ -159,13 +159,15 @@ bool AndroidSurfaceGLSkia::GLContextPresent(const GLPresentInfo& present_info) { if (present_info.presentation_time) { onscreen_surface_->SetPresentationTime(*present_info.presentation_time); } - return onscreen_surface_->SwapBuffers(present_info.damage); + return onscreen_surface_->SwapBuffers(present_info.frame_damage); } -intptr_t AndroidSurfaceGLSkia::GLContextFBO(GLFrameInfo frame_info) const { +GLFBOInfo AndroidSurfaceGLSkia::GLContextFBO(GLFrameInfo frame_info) const { FML_DCHECK(IsValid()); // The default window bound framebuffer on Android. - return 0; + return GLFBOInfo{ + .fbo_id = 0, + }; } // |GPUSurfaceGLDelegate| diff --git a/shell/platform/android/android_surface_gl_skia.h b/shell/platform/android/android_surface_gl_skia.h index 99a910b69b76d..73f36945127f8 100644 --- a/shell/platform/android/android_surface_gl_skia.h +++ b/shell/platform/android/android_surface_gl_skia.h @@ -67,7 +67,7 @@ class AndroidSurfaceGLSkia final : public GPUSurfaceGLDelegate, bool GLContextPresent(const GLPresentInfo& present_info) override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO(GLFrameInfo frame_info) const override; + GLFBOInfo GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| sk_sp GetGLInterface() const override; diff --git a/shell/platform/android/surface/android_surface_mock.cc b/shell/platform/android/surface/android_surface_mock.cc index c3a9f38fc8f0c..8f718fcae667a 100644 --- a/shell/platform/android/surface/android_surface_mock.cc +++ b/shell/platform/android/surface/android_surface_mock.cc @@ -22,8 +22,10 @@ bool AndroidSurfaceMock::GLContextPresent(const GLPresentInfo& present_info) { return true; } -intptr_t AndroidSurfaceMock::GLContextFBO(GLFrameInfo frame_info) const { - return 0; +GLFBOInfo AndroidSurfaceMock::GLContextFBO(GLFrameInfo frame_info) const { + return GLFBOInfo{ + .fbo_id = 0, + }; } } // namespace flutter diff --git a/shell/platform/android/surface/android_surface_mock.h b/shell/platform/android/surface/android_surface_mock.h index d7363873a9a31..3f326a21920b7 100644 --- a/shell/platform/android/surface/android_surface_mock.h +++ b/shell/platform/android/surface/android_surface_mock.h @@ -51,7 +51,7 @@ class AndroidSurfaceMock final : public GPUSurfaceGLDelegate, bool GLContextPresent(const GLPresentInfo& present_info) override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO(GLFrameInfo frame_info) const override; + GLFBOInfo GLContextFBO(GLFrameInfo frame_info) const override; }; } // namespace flutter diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index c733c1a44ec92..04895e9087842 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -132,6 +132,12 @@ static bool IsOpenGLRendererConfigValid(const FlutterRendererConfig* config) { return false; } + if (!SAFE_EXISTS(open_gl_config, populate_existing_damage)) { + FML_LOG(INFO) << "populate_existing_damage was not defined, disabling " + "partial repaint. If you wish to enable partial repaint, " + "please define this callback."; + } + return true; } @@ -222,7 +228,29 @@ static void* DefaultGLProcResolver(const char* name) { } #endif // FML_OS_LINUX || FML_OS_WIN -static flutter::Shell::CreateCallback +#ifdef SHELL_ENABLE_GL +// Auxiliary function used to translate rectangles of type SkIRect to +// FlutterRect. +static FlutterRect SkIRectToFlutterRect(const SkIRect sk_rect) { + FlutterRect flutter_rect = {static_cast(sk_rect.fLeft), + static_cast(sk_rect.fTop), + static_cast(sk_rect.fRight), + static_cast(sk_rect.fBottom)}; + return flutter_rect; +} + +// Auxiliary function used to translate rectangles of type FlutterRect to +// SkIRect. +static const SkIRect FlutterRectToSkIRect(FlutterRect flutter_rect) { + SkIRect rect = {static_cast(flutter_rect.left), + static_cast(flutter_rect.top), + static_cast(flutter_rect.right), + static_cast(flutter_rect.bottom)}; + return rect; +} +#endif + +static inline flutter::Shell::CreateCallback InferOpenGLPlatformViewCreationCallback( const FlutterRendererConfig* config, void* user_data, @@ -241,15 +269,45 @@ InferOpenGLPlatformViewCreationCallback( auto gl_clear_current = [ptr = config->open_gl.clear_current, user_data]() -> bool { return ptr(user_data); }; - auto gl_present = [present = config->open_gl.present, - present_with_info = config->open_gl.present_with_info, - user_data](uint32_t fbo_id) -> bool { + auto gl_present = + [present = config->open_gl.present, + present_with_info = config->open_gl.present_with_info, + user_data](flutter::GLPresentInfo gl_present_info) -> bool { if (present) { return present(user_data); } else { - FlutterPresentInfo present_info = {}; - present_info.struct_size = sizeof(FlutterPresentInfo); - present_info.fbo_id = fbo_id; + // Format the frame and buffer damages accordingly. Note that, since the + // current compute damage algorithm only returns one rectangle for damage + // we are assuming the number of rectangles provided in frame and buffer + // damage are always 1. Once the function that computes damage implements + // support for multiple damage rectangles, GLPresentInfo should also + // contain the number of damage rectangles. + const size_t num_rects = 1; + + std::array frame_damage_rect = { + SkIRectToFlutterRect(*(gl_present_info.frame_damage))}; + std::array buffer_damage_rect = { + SkIRectToFlutterRect(*(gl_present_info.buffer_damage))}; + + FlutterDamage frame_damage{ + .struct_size = sizeof(FlutterDamage), + .num_rects = frame_damage_rect.size(), + .damage = frame_damage_rect.data(), + }; + FlutterDamage buffer_damage{ + .struct_size = sizeof(FlutterDamage), + .num_rects = buffer_damage_rect.size(), + .damage = buffer_damage_rect.data(), + }; + + // Construct the present information concerning the frame being rendered. + FlutterPresentInfo present_info = { + .struct_size = sizeof(FlutterPresentInfo), + .fbo_id = gl_present_info.fbo_id, + .frame_damage = frame_damage, + .buffer_damage = buffer_damage, + }; + return present_with_info(user_data, &present_info); } }; @@ -269,6 +327,50 @@ InferOpenGLPlatformViewCreationCallback( } }; + auto gl_populate_existing_damage = + [populate_existing_damage = config->open_gl.populate_existing_damage, + user_data](intptr_t id) -> flutter::GLFBOInfo { + // If no populate_existing_damage was provided, disable partial + // repaint. + if (!populate_existing_damage) { + return flutter::GLFBOInfo{ + .fbo_id = static_cast(id), + .partial_repaint_enabled = false, + .existing_damage = SkIRect::MakeEmpty(), + }; + } + + // Given the FBO's ID, get its existing damage. + FlutterDamage existing_damage; + populate_existing_damage(user_data, id, &existing_damage); + + bool partial_repaint_enabled = true; + SkIRect existing_damage_rect; + + // Verify that at least one damage rectangle was provided. + if (existing_damage.num_rects <= 0 || existing_damage.damage == nullptr) { + FML_LOG(INFO) << "No damage was provided. Forcing full repaint."; + existing_damage_rect = SkIRect::MakeEmpty(); + partial_repaint_enabled = false; + } else if (existing_damage.num_rects > 1) { + // Log message notifying users that multi-damage is not yet available in + // case they try to make use of it. + FML_LOG(INFO) << "Damage with multiple rectangles not yet supported. " + "Repainting the whole frame."; + existing_damage_rect = SkIRect::MakeEmpty(); + partial_repaint_enabled = false; + } else { + existing_damage_rect = FlutterRectToSkIRect(*(existing_damage.damage)); + } + + // Pass the information about this FBO to the rendering backend. + return flutter::GLFBOInfo{ + .fbo_id = static_cast(id), + .partial_repaint_enabled = partial_repaint_enabled, + .existing_damage = existing_damage_rect, + }; + }; + const FlutterOpenGLRendererConfig* open_gl_config = &config->open_gl; std::function gl_make_resource_current_callback = nullptr; if (SAFE_ACCESS(open_gl_config, make_resource_current, nullptr) != nullptr) { @@ -326,6 +428,7 @@ InferOpenGLPlatformViewCreationCallback( gl_make_resource_current_callback, // gl_make_resource_current_callback gl_surface_transformation_callback, // gl_surface_transformation_callback gl_proc_resolver, // gl_proc_resolver + gl_populate_existing_damage, // gl_populate_existing_damage }; return fml::MakeCopyable( diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 43fd5bd5b1cde..f9aa11b0f877d 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -433,6 +433,16 @@ typedef struct { FlutterSize lower_left_corner_radius; } FlutterRoundedRect; +/// A structure to represent a damage region. +typedef struct { + /// The size of this struct. Must be sizeof(FlutterDamage). + size_t struct_size; + /// The number of rectangles within the damage region. + size_t num_rects; + /// The actual damage region(s) in question. + FlutterRect* damage; +} FlutterDamage; + /// This information is passed to the embedder when requesting a frame buffer /// object. /// @@ -449,6 +459,13 @@ typedef uint32_t (*UIntFrameInfoCallback)( void* /* user data */, const FlutterFrameInfo* /* frame info */); +/// Callback for when a frame buffer object is requested with necessary +/// information for partial repaint. +typedef void (*FlutterFrameBufferWithDamageCallback)( + void* /* user data */, + const intptr_t /* fbo id */, + FlutterDamage* /* existing damage */); + /// This information is passed to the embedder when a surface is presented. /// /// See: \ref FlutterOpenGLRendererConfig.present_with_info. @@ -457,6 +474,10 @@ typedef struct { size_t struct_size; /// Id of the fbo backing the surface that was presented. uint32_t fbo_id; + /// Damage representing the area that the compositor needs to render. + FlutterDamage frame_damage; + /// Damage used to set the buffer's damage region. + FlutterDamage buffer_damage; } FlutterPresentInfo; /// Callback for when a surface is presented. @@ -471,7 +492,10 @@ typedef struct { BoolCallback clear_current; /// Specifying one (and only one) of `present` or `present_with_info` is /// required. Specifying both is an error and engine initialization will be - /// terminated. The return value indicates success of the present call. + /// terminated. The return value indicates success of the present call. If + /// the intent is to use dirty region management, present_with_info must be + /// defined as present will not succeed in communicating information about + /// damage. BoolCallback present; /// Specifying one (and only one) of the `fbo_callback` or /// `fbo_with_frame_info_callback` is required. Specifying both is an error @@ -520,8 +544,27 @@ typedef struct { /// required. Specifying both is an error and engine initialization will be /// terminated. When using this variant, the embedder is passed a /// `FlutterPresentInfo` struct that the embedder can use to release any - /// resources. The return value indicates success of the present call. + /// resources. The return value indicates success of the present call. This + /// callback is essential for dirty region management. If not defined, all the + /// pixels on the screen will be rendered at every frame (regardless of + /// whether damage is actually being computed or not). This is because the + /// information that is passed along to the callback contains the frame and + /// buffer damage that are essential for dirty region management. BoolPresentInfoCallback present_with_info; + /// Specifying this callback is a requirement for dirty region management. + /// Dirty region management will only render the areas of the screen that have + /// changed in between frames, greatly reducing rendering times and energy + /// consumption. To take advantage of these benefits, it is necessary to + /// define populate_existing_damage as a callback that takes user + /// data, an FBO ID, and an existing damage FlutterDamage. The callback should + /// use the given FBO ID to identify the FBO's exisiting damage (i.e. areas + /// that have changed since the FBO was last used) and use it to populate the + /// given existing damage variable. This callback is dependent on either + /// fbo_callback or fbo_with_frame_info_callback being defined as they are + /// responsible for providing populate_existing_damage with the FBO's + /// ID. Not specifying populate_existing_damage will result in full + /// repaint (i.e. rendering all the pixels on the screen at every frame). + FlutterFrameBufferWithDamageCallback populate_existing_damage; } FlutterOpenGLRendererConfig; /// Alias for id. diff --git a/shell/platform/embedder/embedder_surface_gl.cc b/shell/platform/embedder/embedder_surface_gl.cc index e406ee9ed10fa..88cdb316c5e5b 100644 --- a/shell/platform/embedder/embedder_surface_gl.cc +++ b/shell/platform/embedder/embedder_surface_gl.cc @@ -19,7 +19,8 @@ EmbedderSurfaceGL::EmbedderSurfaceGL( if (!gl_dispatch_table_.gl_make_current_callback || !gl_dispatch_table_.gl_clear_current_callback || !gl_dispatch_table_.gl_present_callback || - !gl_dispatch_table_.gl_fbo_callback) { + !gl_dispatch_table_.gl_fbo_callback || + !gl_dispatch_table_.gl_populate_existing_damage) { return; } @@ -46,12 +47,16 @@ bool EmbedderSurfaceGL::GLContextClearCurrent() { // |GPUSurfaceGLDelegate| bool EmbedderSurfaceGL::GLContextPresent(const GLPresentInfo& present_info) { - return gl_dispatch_table_.gl_present_callback(present_info.fbo_id); + // Pass the present information to the embedder present callback. + return gl_dispatch_table_.gl_present_callback(present_info); } // |GPUSurfaceGLDelegate| -intptr_t EmbedderSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { - return gl_dispatch_table_.gl_fbo_callback(frame_info); +GLFBOInfo EmbedderSurfaceGL::GLContextFBO(GLFrameInfo frame_info) const { + // Get the FBO ID using the gl_fbo_callback and then get exiting damage by + // passing that ID to the gl_populate_existing_damage. + return gl_dispatch_table_.gl_populate_existing_damage( + gl_dispatch_table_.gl_fbo_callback(frame_info)); } // |GPUSurfaceGLDelegate| diff --git a/shell/platform/embedder/embedder_surface_gl.h b/shell/platform/embedder/embedder_surface_gl.h index c0a2b6a55d98f..583f0f469a0e7 100644 --- a/shell/platform/embedder/embedder_surface_gl.h +++ b/shell/platform/embedder/embedder_surface_gl.h @@ -18,12 +18,13 @@ class EmbedderSurfaceGL final : public EmbedderSurface, struct GLDispatchTable { std::function gl_make_current_callback; // required std::function gl_clear_current_callback; // required - std::function gl_present_callback; // required + std::function gl_present_callback; // required std::function gl_fbo_callback; // required std::function gl_make_resource_current_callback; // optional std::function - gl_surface_transformation_callback; // optional - std::function gl_proc_resolver; // optional + gl_surface_transformation_callback; // optional + std::function gl_proc_resolver; // optional + std::function gl_populate_existing_damage; // required }; EmbedderSurfaceGL( @@ -59,7 +60,7 @@ class EmbedderSurfaceGL final : public EmbedderSurface, bool GLContextPresent(const GLPresentInfo& present_info) override; // |GPUSurfaceGLDelegate| - intptr_t GLContextFBO(GLFrameInfo frame_info) const override; + GLFBOInfo GLContextFBO(GLFrameInfo frame_info) const override; // |GPUSurfaceGLDelegate| bool GLContextFBOResetAfterPresent() const override; diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 918abff4e261e..58e48c4b594d8 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -52,13 +52,14 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( opengl_renderer_config_.present_with_info = [](void* context, const FlutterPresentInfo* present_info) -> bool { return reinterpret_cast(context)->GLPresent( - present_info->fbo_id); + *present_info); }; opengl_renderer_config_.fbo_with_frame_info_callback = [](void* context, const FlutterFrameInfo* frame_info) -> uint32_t { return reinterpret_cast(context)->GLGetFramebuffer( *frame_info); }; + opengl_renderer_config_.populate_existing_damage = nullptr; opengl_renderer_config_.make_resource_current = [](void* context) -> bool { return reinterpret_cast(context) ->GLMakeResourceCurrent(); @@ -160,7 +161,10 @@ void EmbedderConfigBuilder::SetOpenGLPresentCallBack() { FML_CHECK(renderer_config_.type == FlutterRendererType::kOpenGL); renderer_config_.open_gl.present = [](void* context) -> bool { // passing a placeholder fbo_id. - return reinterpret_cast(context)->GLPresent(0); + return reinterpret_cast(context)->GLPresent( + FlutterPresentInfo{ + .fbo_id = 0, + }); }; #endif } @@ -312,6 +316,10 @@ void EmbedderConfigBuilder::SetupVsyncCallback() { }; } +FlutterRendererConfig& EmbedderConfigBuilder::GetRendererConfig() { + return renderer_config_; +} + void EmbedderConfigBuilder::SetRenderTaskRunner( const FlutterTaskRunnerDescription* runner) { if (runner == nullptr) { diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index 3ce230c0146b8..2727c85fdadbd 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -104,6 +104,8 @@ class EmbedderConfigBuilder { FlutterCompositor& GetCompositor(); + FlutterRendererConfig& GetRendererConfig(); + void SetRenderTargetType( EmbedderTestBackingStoreProducer::RenderTargetType type, FlutterSoftwarePixelFormat software_pixfmt = kNative32); diff --git a/shell/platform/embedder/tests/embedder_test_context_gl.cc b/shell/platform/embedder/tests/embedder_test_context_gl.cc index 9a7bc9bcf1c26..0215999814a0d 100644 --- a/shell/platform/embedder/tests/embedder_test_context_gl.cc +++ b/shell/platform/embedder/tests/embedder_test_context_gl.cc @@ -39,7 +39,7 @@ bool EmbedderTestContextGL::GLClearCurrent() { return gl_surface_->ClearCurrent(); } -bool EmbedderTestContextGL::GLPresent(uint32_t fbo_id) { +bool EmbedderTestContextGL::GLPresent(FlutterPresentInfo present_info) { FML_CHECK(gl_surface_) << "GL surface must be initialized."; gl_surface_present_count_++; @@ -50,7 +50,7 @@ bool EmbedderTestContextGL::GLPresent(uint32_t fbo_id) { } if (callback) { - callback(fbo_id); + callback(present_info); } FireRootSurfacePresentCallbackIfPresent( @@ -64,6 +64,12 @@ void EmbedderTestContextGL::SetGLGetFBOCallback(GLGetFBOCallback callback) { gl_get_fbo_callback_ = callback; } +void EmbedderTestContextGL::SetGLPopulateExistingDamageCallback( + GLPopulateExistingDamageCallback callback) { + std::scoped_lock lock(gl_callback_mutex_); + gl_populate_existing_damage_callback_ = callback; +} + void EmbedderTestContextGL::SetGLPresentCallback(GLPresentCallback callback) { std::scoped_lock lock(gl_callback_mutex_); gl_present_callback_ = callback; @@ -86,6 +92,22 @@ uint32_t EmbedderTestContextGL::GLGetFramebuffer(FlutterFrameInfo frame_info) { return gl_surface_->GetFramebuffer(size.width, size.height); } +void EmbedderTestContextGL::GLPopulateExistingDamage( + const intptr_t id, + FlutterDamage* existing_damage) { + FML_CHECK(gl_surface_) << "GL surface must be initialized."; + + GLPopulateExistingDamageCallback callback; + { + std::scoped_lock lock(gl_callback_mutex_); + callback = gl_populate_existing_damage_callback_; + } + + if (callback) { + callback(id, existing_damage); + } +} + bool EmbedderTestContextGL::GLMakeResourceCurrent() { FML_CHECK(gl_surface_) << "GL surface must be initialized."; return gl_surface_->MakeResourceCurrent(); diff --git a/shell/platform/embedder/tests/embedder_test_context_gl.h b/shell/platform/embedder/tests/embedder_test_context_gl.h index 65e2c34c355c2..9b91ef5868868 100644 --- a/shell/platform/embedder/tests/embedder_test_context_gl.h +++ b/shell/platform/embedder/tests/embedder_test_context_gl.h @@ -14,7 +14,10 @@ namespace testing { class EmbedderTestContextGL : public EmbedderTestContext { public: using GLGetFBOCallback = std::function; - using GLPresentCallback = std::function; + using GLPopulateExistingDamageCallback = + std::function; + using GLPresentCallback = + std::function; explicit EmbedderTestContextGL(std::string assets_path = ""); @@ -38,6 +41,9 @@ class EmbedderTestContextGL : public EmbedderTestContext { /// void SetGLGetFBOCallback(GLGetFBOCallback callback); + void SetGLPopulateExistingDamageCallback( + GLPopulateExistingDamageCallback callback); + uint32_t GetWindowFBOId() const; //---------------------------------------------------------------------------- @@ -53,6 +59,9 @@ class EmbedderTestContextGL : public EmbedderTestContext { /// void SetGLPresentCallback(GLPresentCallback callback); + void GLPopulateExistingDamage(const intptr_t id, + FlutterDamage* existing_damage); + protected: virtual void SetupCompositor() override; @@ -65,6 +74,7 @@ class EmbedderTestContextGL : public EmbedderTestContext { std::mutex gl_callback_mutex_; GLGetFBOCallback gl_get_fbo_callback_; GLPresentCallback gl_present_callback_; + GLPopulateExistingDamageCallback gl_populate_existing_damage_callback_; void SetupSurface(SkISize surface_size) override; @@ -72,7 +82,7 @@ class EmbedderTestContextGL : public EmbedderTestContext { bool GLClearCurrent(); - bool GLPresent(uint32_t fbo_id); + bool GLPresent(FlutterPresentInfo present_info); uint32_t GLGetFramebuffer(FlutterFrameInfo frame_info); diff --git a/shell/platform/embedder/tests/embedder_unittests_gl.cc b/shell/platform/embedder/tests/embedder_unittests_gl.cc index 9174461529058..76cf6102cdf60 100644 --- a/shell/platform/embedder/tests/embedder_unittests_gl.cc +++ b/shell/platform/embedder/tests/embedder_unittests_gl.cc @@ -3106,6 +3106,72 @@ TEST_F(EmbedderTest, MustNotRunWithBothPresentCallbacksSet) { ASSERT_FALSE(engine.is_valid()); } +TEST_F(EmbedderTest, MustStillRunWhenPopulateExistingDamageIsNotProvided) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(1, 1)); + builder.GetRendererConfig().open_gl.populate_existing_damage = nullptr; + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); +} + +TEST_F(EmbedderTest, MustRunWhenPopulateExistingDamageIsProvided) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(1, 1)); + + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); +} + +TEST_F(EmbedderTest, MustRunWithPopulateExistingDamageAndFBOCallback) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(1, 1)); + builder.GetRendererConfig().open_gl.fbo_callback = + [](void* context) -> uint32_t { return 0; }; + builder.GetRendererConfig().open_gl.fbo_with_frame_info_callback = nullptr; + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); +} + +TEST_F(EmbedderTest, + MustNotRunWhenPopulateExistingDamageButNoOtherFBOCallback) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(1, 1)); + builder.GetRendererConfig().open_gl.fbo_callback = nullptr; + builder.GetRendererConfig().open_gl.fbo_with_frame_info_callback = nullptr; + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + auto engine = builder.LaunchEngine(); + ASSERT_FALSE(engine.is_valid()); +} + TEST_F(EmbedderTest, PresentInfoContainsValidFBOId) { auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); @@ -3140,8 +3206,8 @@ TEST_F(EmbedderTest, PresentInfoContainsValidFBOId) { const uint32_t window_fbo_id = static_cast(context).GetWindowFBOId(); static_cast(context).SetGLPresentCallback( - [window_fbo_id = window_fbo_id](uint32_t fbo_id) { - ASSERT_EQ(fbo_id, window_fbo_id); + [window_fbo_id = window_fbo_id](FlutterPresentInfo present_info) { + ASSERT_EQ(present_info.fbo_id, window_fbo_id); frame_latch.CountDown(); }); @@ -3149,6 +3215,315 @@ TEST_F(EmbedderTest, PresentInfoContainsValidFBOId) { frame_latch.Wait(); } +TEST_F(EmbedderTest, + PresentInfoReceivesFullDamageWhenExistingDamageIsWholeScreen) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetDartEntrypoint("render_gradient"); + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + // Return existing damage as the entire screen on purpose. + static_cast(context) + .SetGLPopulateExistingDamageCallback( + [](const intptr_t id, FlutterDamage* existing_damage_ptr) { + const size_t num_rects = 1; + FlutterRect existing_damage_rects[num_rects] = { + FlutterRect{0, 0, 800, 600}}; + existing_damage_ptr->num_rects = num_rects; + existing_damage_ptr->damage = existing_damage_rects; + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // First frame should be entirely rerendered. + static_cast(context).SetGLPresentCallback( + [](FlutterPresentInfo present_info) { + const size_t num_rects = 1; + ASSERT_EQ(present_info.frame_damage.num_rects, num_rects); + ASSERT_EQ(present_info.frame_damage.damage->left, 0); + ASSERT_EQ(present_info.frame_damage.damage->top, 0); + ASSERT_EQ(present_info.frame_damage.damage->right, 800); + ASSERT_EQ(present_info.frame_damage.damage->bottom, 600); + + ASSERT_EQ(present_info.buffer_damage.num_rects, num_rects); + ASSERT_EQ(present_info.buffer_damage.damage->left, 0); + ASSERT_EQ(present_info.buffer_damage.damage->top, 0); + ASSERT_EQ(present_info.buffer_damage.damage->right, 800); + ASSERT_EQ(present_info.buffer_damage.damage->bottom, 600); + }); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + // Because it's the same as the first frame, the second frame damage should + // be empty but, because there was a full existing buffer damage, the buffer + // damage should be the entire screen. + static_cast(context).SetGLPresentCallback( + [](FlutterPresentInfo present_info) { + const size_t num_rects = 1; + ASSERT_EQ(present_info.frame_damage.num_rects, num_rects); + ASSERT_EQ(present_info.frame_damage.damage->left, 0); + ASSERT_EQ(present_info.frame_damage.damage->top, 0); + ASSERT_EQ(present_info.frame_damage.damage->right, 0); + ASSERT_EQ(present_info.frame_damage.damage->bottom, 0); + + ASSERT_EQ(present_info.buffer_damage.num_rects, num_rects); + ASSERT_EQ(present_info.buffer_damage.damage->left, 0); + ASSERT_EQ(present_info.buffer_damage.damage->top, 0); + ASSERT_EQ(present_info.buffer_damage.damage->right, 800); + ASSERT_EQ(present_info.buffer_damage.damage->bottom, 600); + }); + + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); +} + +TEST_F(EmbedderTest, PresentInfoReceivesEmptyDamage) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetDartEntrypoint("render_gradient"); + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + // Return no existing damage on purpose. + static_cast(context) + .SetGLPopulateExistingDamageCallback( + [](const intptr_t id, FlutterDamage* existing_damage_ptr) { + const size_t num_rects = 1; + FlutterRect existing_damage_rects[num_rects] = { + FlutterRect{0, 0, 0, 0}}; + existing_damage_ptr->num_rects = num_rects; + existing_damage_ptr->damage = existing_damage_rects; + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // First frame should be entirely rerendered. + static_cast(context).SetGLPresentCallback( + [](FlutterPresentInfo present_info) { + const size_t num_rects = 1; + ASSERT_EQ(present_info.frame_damage.num_rects, num_rects); + ASSERT_EQ(present_info.frame_damage.damage->left, 0); + ASSERT_EQ(present_info.frame_damage.damage->top, 0); + ASSERT_EQ(present_info.frame_damage.damage->right, 800); + ASSERT_EQ(present_info.frame_damage.damage->bottom, 600); + + ASSERT_EQ(present_info.buffer_damage.num_rects, num_rects); + ASSERT_EQ(present_info.buffer_damage.damage->left, 0); + ASSERT_EQ(present_info.buffer_damage.damage->top, 0); + ASSERT_EQ(present_info.buffer_damage.damage->right, 800); + ASSERT_EQ(present_info.buffer_damage.damage->bottom, 600); + }); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + // Because it's the same as the first frame, the second frame should not be + // rerendered assuming there is no existing damage. + static_cast(context).SetGLPresentCallback( + [](FlutterPresentInfo present_info) { + const size_t num_rects = 1; + ASSERT_EQ(present_info.frame_damage.num_rects, num_rects); + ASSERT_EQ(present_info.frame_damage.damage->left, 0); + ASSERT_EQ(present_info.frame_damage.damage->top, 0); + ASSERT_EQ(present_info.frame_damage.damage->right, 0); + ASSERT_EQ(present_info.frame_damage.damage->bottom, 0); + + ASSERT_EQ(present_info.buffer_damage.num_rects, num_rects); + ASSERT_EQ(present_info.buffer_damage.damage->left, 0); + ASSERT_EQ(present_info.buffer_damage.damage->top, 0); + ASSERT_EQ(present_info.buffer_damage.damage->right, 0); + ASSERT_EQ(present_info.buffer_damage.damage->bottom, 0); + }); + + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); +} + +TEST_F(EmbedderTest, PresentInfoReceivesPartialDamage) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetDartEntrypoint("render_gradient"); + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + // Return existing damage as only part of the screen on purpose. + static_cast(context) + .SetGLPopulateExistingDamageCallback( + [](const intptr_t id, FlutterDamage* existing_damage_ptr) { + const size_t num_rects = 1; + FlutterRect existing_damage_rects[num_rects] = { + FlutterRect{200, 150, 400, 300}}; + existing_damage_ptr->num_rects = num_rects; + existing_damage_ptr->damage = existing_damage_rects; + }); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // First frame should be entirely rerendered. + static_cast(context).SetGLPresentCallback( + [](FlutterPresentInfo present_info) { + const size_t num_rects = 1; + ASSERT_EQ(present_info.frame_damage.num_rects, num_rects); + ASSERT_EQ(present_info.frame_damage.damage->left, 0); + ASSERT_EQ(present_info.frame_damage.damage->top, 0); + ASSERT_EQ(present_info.frame_damage.damage->right, 800); + ASSERT_EQ(present_info.frame_damage.damage->bottom, 600); + + ASSERT_EQ(present_info.buffer_damage.num_rects, num_rects); + ASSERT_EQ(present_info.buffer_damage.damage->left, 0); + ASSERT_EQ(present_info.buffer_damage.damage->top, 0); + ASSERT_EQ(present_info.buffer_damage.damage->right, 800); + ASSERT_EQ(present_info.buffer_damage.damage->bottom, 600); + }); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); + + // Because it's the same as the first frame, the second frame damage should be + // empty but, because there was a partial existing damage, the buffer damage + // should represent that partial damage area. + static_cast(context).SetGLPresentCallback( + [](FlutterPresentInfo present_info) { + const size_t num_rects = 1; + ASSERT_EQ(present_info.frame_damage.num_rects, num_rects); + ASSERT_EQ(present_info.frame_damage.damage->left, 0); + ASSERT_EQ(present_info.frame_damage.damage->top, 0); + ASSERT_EQ(present_info.frame_damage.damage->right, 0); + ASSERT_EQ(present_info.frame_damage.damage->bottom, 0); + + ASSERT_EQ(present_info.buffer_damage.num_rects, num_rects); + ASSERT_EQ(present_info.buffer_damage.damage->left, 200); + ASSERT_EQ(present_info.buffer_damage.damage->top, 150); + ASSERT_EQ(present_info.buffer_damage.damage->right, 400); + ASSERT_EQ(present_info.buffer_damage.damage->bottom, 300); + }); + + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); +} + +TEST_F(EmbedderTest, PopulateExistingDamageReceivesValidID) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetDartEntrypoint("render_gradient"); + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + const uint32_t window_fbo_id = + static_cast(context).GetWindowFBOId(); + static_cast(context) + .SetGLPopulateExistingDamageCallback( + [window_fbo_id = window_fbo_id](intptr_t id, + FlutterDamage* existing_damage) { + ASSERT_EQ(id, window_fbo_id); + }); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); +} + +TEST_F(EmbedderTest, PopulateExistingDamageReceivesInvalidID) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetDartEntrypoint("render_gradient"); + builder.GetRendererConfig().open_gl.populate_existing_damage = + [](void* context, const intptr_t id, + FlutterDamage* existing_damage) -> void { + return reinterpret_cast(context) + ->GLPopulateExistingDamage(id, existing_damage); + }; + + // Return a bad FBO ID on purpose. + builder.GetRendererConfig().open_gl.fbo_with_frame_info_callback = + [](void* context, const FlutterFrameInfo* frame_info) -> uint32_t { + return 123; + }; + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + context.AddNativeCallback("SignalNativeTest", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + /* Nothing to do. */ + })); + + const uint32_t window_fbo_id = + static_cast(context).GetWindowFBOId(); + static_cast(context) + .SetGLPopulateExistingDamageCallback( + [window_fbo_id = window_fbo_id](intptr_t id, + FlutterDamage* existing_damage) { + ASSERT_NE(id, window_fbo_id); + }); + + // Send a window metrics events so frames may be scheduled. + FlutterWindowMetricsEvent event = {}; + event.struct_size = sizeof(event); + event.width = 800; + event.height = 600; + event.pixel_ratio = 1.0; + ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), + kSuccess); +} + TEST_F(EmbedderTest, SetSingleDisplayConfigurationWithDisplayId) { auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext);