Enable dirty region management within the Embedder API (#35022)

diff --git a/shell/common/shell_test_platform_view_gl.cc b/shell/common/shell_test_platform_view_gl.cc
index 4923542..ac68ff3 100644
--- a/shell/common/shell_test_platform_view_gl.cc
+++ b/shell/common/shell_test_platform_view_gl.cc
@@ -69,8 +69,10 @@
 }
 
 // |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 c890c1e..b4afc12 100644
--- a/shell/common/shell_test_platform_view_gl.h
+++ b/shell/common/shell_test_platform_view_gl.h
@@ -61,7 +61,7 @@
   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 f9da6a1..d1e31b9 100644
--- a/shell/gpu/gpu_surface_gl_delegate.h
+++ b/shell/gpu/gpu_surface_gl_delegate.h
@@ -22,17 +22,32 @@
   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<SkIRect>& damage;
+  // The frame damage is a hint to compositor telling it which parts of front
+  // buffer need to be updated.
+  const std::optional<SkIRect>& 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<fml::TimePoint> 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<SkIRect>& buffer_damage;
 };
 
 class GPUSurfaceGLDelegate {
@@ -54,8 +69,9 @@
   // 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 2441314..3fc7c44 100644
--- a/shell/gpu/gpu_surface_gl_impeller.cc
+++ b/shell/gpu/gpu_surface_gl_impeller.cc
@@ -62,10 +62,11 @@
     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 66bb663..eafb612 100644
--- a/shell/gpu/gpu_surface_gl_skia.cc
+++ b/shell/gpu/gpu_surface_gl_skia.cc
@@ -181,10 +181,10 @@
 
   GLFrameInfo frame_info = {static_cast<uint32_t>(size.width()),
                             static_cast<uint32_t>(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 @@
   }
 
   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 @@
       };
 
   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<SurfaceFrame>(surface, std::move(framebuffer_info),
                                         submit_callback,
                                         std::move(context_switch));
@@ -268,8 +273,9 @@
 
   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 @@
 
     // 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 @@
     }
 
     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 90cad24..39f6e3f 100644
--- a/shell/gpu/gpu_surface_gl_skia.h
+++ b/shell/gpu/gpu_surface_gl_skia.h
@@ -67,6 +67,8 @@
   sk_sp<SkSurface> 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 @@
   // 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<GPUSurfaceGLSkia> weak_factory_;
diff --git a/shell/platform/android/android_surface_gl_impeller.cc b/shell/platform/android/android_surface_gl_impeller.cc
index 28c1cfe..635acc7 100644
--- a/shell/platform/android/android_surface_gl_impeller.cc
+++ b/shell/platform/android/android_surface_gl_impeller.cc
@@ -295,9 +295,11 @@
 }
 
 // |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 8bcf14d..9d23998 100644
--- a/shell/platform/android/android_surface_gl_impeller.h
+++ b/shell/platform/android/android_surface_gl_impeller.h
@@ -68,7 +68,7 @@
   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<const GrGLInterface> GetGLInterface() const override;
diff --git a/shell/platform/android/android_surface_gl_skia.cc b/shell/platform/android/android_surface_gl_skia.cc
index 23242c9..50ac308 100644
--- a/shell/platform/android/android_surface_gl_skia.cc
+++ b/shell/platform/android/android_surface_gl_skia.cc
@@ -159,13 +159,15 @@
   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 99a910b..73f3694 100644
--- a/shell/platform/android/android_surface_gl_skia.h
+++ b/shell/platform/android/android_surface_gl_skia.h
@@ -67,7 +67,7 @@
   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<const GrGLInterface> GetGLInterface() const override;
diff --git a/shell/platform/android/surface/android_surface_mock.cc b/shell/platform/android/surface/android_surface_mock.cc
index c3a9f38..8f718fc 100644
--- a/shell/platform/android/surface/android_surface_mock.cc
+++ b/shell/platform/android/surface/android_surface_mock.cc
@@ -22,8 +22,10 @@
   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 d736387..3f326a2 100644
--- a/shell/platform/android/surface/android_surface_mock.h
+++ b/shell/platform/android/surface/android_surface_mock.h
@@ -51,7 +51,7 @@
   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 0868901..97fced2 100644
--- a/shell/platform/embedder/embedder.cc
+++ b/shell/platform/embedder/embedder.cc
@@ -132,6 +132,12 @@
     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 @@
 }
 #endif  // FML_OS_LINUX || FML_OS_WIN
 
-static flutter::Shell::CreateCallback<flutter::PlatformView>
+#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<double>(sk_rect.fLeft),
+                              static_cast<double>(sk_rect.fTop),
+                              static_cast<double>(sk_rect.fRight),
+                              static_cast<double>(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<int32_t>(flutter_rect.left),
+                  static_cast<int32_t>(flutter_rect.top),
+                  static_cast<int32_t>(flutter_rect.right),
+                  static_cast<int32_t>(flutter_rect.bottom)};
+  return rect;
+}
+#endif
+
+static inline flutter::Shell::CreateCallback<flutter::PlatformView>
 InferOpenGLPlatformViewCreationCallback(
     const FlutterRendererConfig* config,
     void* user_data,
@@ -241,15 +269,45 @@
   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<FlutterRect, num_rects> frame_damage_rect = {
+          SkIRectToFlutterRect(*(gl_present_info.frame_damage))};
+      std::array<FlutterRect, num_rects> 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 @@
     }
   };
 
+  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<uint32_t>(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<uint32_t>(id),
+        .partial_repaint_enabled = partial_repaint_enabled,
+        .existing_damage = existing_damage_rect,
+    };
+  };
+
   const FlutterOpenGLRendererConfig* open_gl_config = &config->open_gl;
   std::function<bool()> gl_make_resource_current_callback = nullptr;
   if (SAFE_ACCESS(open_gl_config, make_resource_current, nullptr) != nullptr) {
@@ -326,6 +428,7 @@
       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 88a3014..93e92b7 100644
--- a/shell/platform/embedder/embedder.h
+++ b/shell/platform/embedder/embedder.h
@@ -433,6 +433,16 @@
   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 @@
     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 @@
   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 @@
   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 @@
   /// 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<MTLDevice>.
diff --git a/shell/platform/embedder/embedder_surface_gl.cc b/shell/platform/embedder/embedder_surface_gl.cc
index e406ee9..88cdb31 100644
--- a/shell/platform/embedder/embedder_surface_gl.cc
+++ b/shell/platform/embedder/embedder_surface_gl.cc
@@ -19,7 +19,8 @@
   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 @@
 
 // |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 c0a2b6a..583f0f4 100644
--- a/shell/platform/embedder/embedder_surface_gl.h
+++ b/shell/platform/embedder/embedder_surface_gl.h
@@ -18,12 +18,13 @@
   struct GLDispatchTable {
     std::function<bool(void)> gl_make_current_callback;           // required
     std::function<bool(void)> gl_clear_current_callback;          // required
-    std::function<bool(uint32_t)> gl_present_callback;            // required
+    std::function<bool(GLPresentInfo)> gl_present_callback;       // required
     std::function<intptr_t(GLFrameInfo)> gl_fbo_callback;         // required
     std::function<bool(void)> gl_make_resource_current_callback;  // optional
     std::function<SkMatrix(void)>
-        gl_surface_transformation_callback;              // optional
-    std::function<void*(const char*)> gl_proc_resolver;  // optional
+        gl_surface_transformation_callback;                          // optional
+    std::function<void*(const char*)> gl_proc_resolver;              // optional
+    std::function<GLFBOInfo(intptr_t)> gl_populate_existing_damage;  // required
   };
 
   EmbedderSurfaceGL(
@@ -59,7 +60,7 @@
   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 918abff..58e48c4 100644
--- a/shell/platform/embedder/tests/embedder_config_builder.cc
+++ b/shell/platform/embedder/tests/embedder_config_builder.cc
@@ -52,13 +52,14 @@
   opengl_renderer_config_.present_with_info =
       [](void* context, const FlutterPresentInfo* present_info) -> bool {
     return reinterpret_cast<EmbedderTestContextGL*>(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<EmbedderTestContextGL*>(context)->GLGetFramebuffer(
         *frame_info);
   };
+  opengl_renderer_config_.populate_existing_damage = nullptr;
   opengl_renderer_config_.make_resource_current = [](void* context) -> bool {
     return reinterpret_cast<EmbedderTestContextGL*>(context)
         ->GLMakeResourceCurrent();
@@ -160,7 +161,10 @@
   FML_CHECK(renderer_config_.type == FlutterRendererType::kOpenGL);
   renderer_config_.open_gl.present = [](void* context) -> bool {
     // passing a placeholder fbo_id.
-    return reinterpret_cast<EmbedderTestContextGL*>(context)->GLPresent(0);
+    return reinterpret_cast<EmbedderTestContextGL*>(context)->GLPresent(
+        FlutterPresentInfo{
+            .fbo_id = 0,
+        });
   };
 #endif
 }
@@ -312,6 +316,10 @@
   };
 }
 
+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 3ce230c..2727c85 100644
--- a/shell/platform/embedder/tests/embedder_config_builder.h
+++ b/shell/platform/embedder/tests/embedder_config_builder.h
@@ -104,6 +104,8 @@
 
   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 9a7bc9b..0215999 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 @@
   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 @@
   }
 
   if (callback) {
-    callback(fbo_id);
+    callback(present_info);
   }
 
   FireRootSurfacePresentCallbackIfPresent(
@@ -64,6 +64,12 @@
   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 @@
   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 65e2c34..9b91ef5 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 @@
 class EmbedderTestContextGL : public EmbedderTestContext {
  public:
   using GLGetFBOCallback = std::function<void(FlutterFrameInfo frame_info)>;
-  using GLPresentCallback = std::function<void(uint32_t fbo_id)>;
+  using GLPopulateExistingDamageCallback =
+      std::function<void(intptr_t id, FlutterDamage* existing_damage)>;
+  using GLPresentCallback =
+      std::function<void(FlutterPresentInfo present_info)>;
 
   explicit EmbedderTestContextGL(std::string assets_path = "");
 
@@ -38,6 +41,9 @@
   ///
   void SetGLGetFBOCallback(GLGetFBOCallback callback);
 
+  void SetGLPopulateExistingDamageCallback(
+      GLPopulateExistingDamageCallback callback);
+
   uint32_t GetWindowFBOId() const;
 
   //----------------------------------------------------------------------------
@@ -53,6 +59,9 @@
   ///
   void SetGLPresentCallback(GLPresentCallback callback);
 
+  void GLPopulateExistingDamage(const intptr_t id,
+                                FlutterDamage* existing_damage);
+
  protected:
   virtual void SetupCompositor() override;
 
@@ -65,6 +74,7 @@
   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 @@
 
   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 9174461..76cf610 100644
--- a/shell/platform/embedder/tests/embedder_unittests_gl.cc
+++ b/shell/platform/embedder/tests/embedder_unittests_gl.cc
@@ -3106,6 +3106,72 @@
   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<EmbedderTestContextGL*>(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<EmbedderTestContextGL*>(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<EmbedderTestContextGL*>(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 @@
   const uint32_t window_fbo_id =
       static_cast<EmbedderTestContextGL&>(context).GetWindowFBOId();
   static_cast<EmbedderTestContextGL&>(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 @@
   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<EmbedderTestContextGL*>(context)
+        ->GLPopulateExistingDamage(id, existing_damage);
+  };
+
+  // Return existing damage as the entire screen on purpose.
+  static_cast<EmbedderTestContextGL&>(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<EmbedderTestContextGL&>(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<EmbedderTestContextGL&>(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<EmbedderTestContextGL*>(context)
+        ->GLPopulateExistingDamage(id, existing_damage);
+  };
+
+  // Return no existing damage on purpose.
+  static_cast<EmbedderTestContextGL&>(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<EmbedderTestContextGL&>(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<EmbedderTestContextGL&>(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<EmbedderTestContextGL*>(context)
+        ->GLPopulateExistingDamage(id, existing_damage);
+  };
+
+  // Return existing damage as only part of the screen on purpose.
+  static_cast<EmbedderTestContextGL&>(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<EmbedderTestContextGL&>(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<EmbedderTestContextGL&>(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<EmbedderTestContextGL*>(context)
+        ->GLPopulateExistingDamage(id, existing_damage);
+  };
+
+  auto engine = builder.LaunchEngine();
+  ASSERT_TRUE(engine.is_valid());
+
+  const uint32_t window_fbo_id =
+      static_cast<EmbedderTestContextGL&>(context).GetWindowFBOId();
+  static_cast<EmbedderTestContextGL&>(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<EmbedderTestContextGL*>(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<EmbedderTestContextGL&>(context).GetWindowFBOId();
+  static_cast<EmbedderTestContextGL&>(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);