PlatformViewsController: clear composition_order_ in the beginning of each frame. (#22574) (#22762)

Co-authored-by: Chris Yang <ychris@google.com>
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
index feb58fb..90786cb 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
@@ -241,13 +241,13 @@
   gesture_recognizers_blocking_policies[idString] = gestureRecognizerBlockingPolicy;
 }
 
-void FlutterPlatformViewsController::SetFrameSize(SkISize frame_size) {
+void FlutterPlatformViewsController::BeginFrame(SkISize frame_size) {
+  ResetFrameState();
   frame_size_ = frame_size;
 }
 
 void FlutterPlatformViewsController::CancelFrame() {
-  picture_recorders_.clear();
-  composition_order_.clear();
+  ResetFrameState();
 }
 
 // TODO(cyanglaz): https://github.com/flutter/flutter/issues/56474
@@ -603,13 +603,6 @@
   }
 }
 
-void FlutterPlatformViewsController::EndFrame(
-    bool should_resubmit_frame,
-    fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
-  // Reset the composition order, so next frame starts empty.
-  composition_order_.clear();
-}
-
 std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewsController::GetLayer(
     GrDirectContext* gr_context,
     std::shared_ptr<IOSContext> ios_context,
@@ -705,6 +698,11 @@
   }
 }
 
+void FlutterPlatformViewsController::ResetFrameState() {
+  picture_recorders_.clear();
+  composition_order_.clear();
+}
+
 }  // namespace flutter
 
 // This recognizers delays touch events from being dispatched to the responder chain until it failed
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
index 31a19b2..55a143e 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
@@ -136,8 +136,6 @@
       result);
 
   XCTAssertNotNil(gMockPlatformView);
-
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testChildClippingViewHitTests {
@@ -213,7 +211,6 @@
   CGRect platformViewRectInFlutterView = [gMockPlatformView convertRect:gMockPlatformView.bounds
                                                                  toView:mockFlutterView];
   XCTAssertTrue(CGRectEqualToRect(platformViewRectInFlutterView, CGRectMake(100, 100, 300, 300)));
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testChildClippingViewShouldBeTheBoundingRectOfPlatformView {
@@ -283,8 +280,6 @@
   XCTAssertLessThan(
       fabs(platformViewRectInFlutterView.size.height - childClippingView.frame.size.height),
       kFloatCompareEpsilon);
-
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testClipRect {
@@ -356,7 +351,6 @@
       }
     }
   }
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testClipRRect {
@@ -428,7 +422,6 @@
       }
     }
   }
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testClipPath {
@@ -501,7 +494,6 @@
       }
     }
   }
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testSetFlutterViewControllerAfterCreateCanStillDispatchTouchEvents {
@@ -564,8 +556,6 @@
   flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller);
   [forwardGectureRecognizer touchesBegan:touches2 withEvent:event2];
   OCMVerify([mockFlutterViewContoller touchesBegan:touches2 withEvent:event2]);
-
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)testSetFlutterViewControllerInTheMiddleOfTouchEventShouldStillAllowGesturesToBeHandled {
@@ -816,8 +806,6 @@
   gpu_is_disabled->SetSwitch(false);
   XCTAssertTrue(flutterPlatformViewsController->SubmitFrame(
       nullptr, nullptr, std::move(mock_surface_submit_false), gpu_is_disabled));
-
-  flutterPlatformViewsController->Reset();
 }
 
 - (void)
@@ -869,6 +857,59 @@
   XCTAssertNil(gMockPlatformView);
 }
 
+- (void)testFlutterPlatformViewControllerBeginFrameShouldResetCompisitionOrder {
+  flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
+  auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
+  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
+                               /*platform=*/thread_task_runner,
+                               /*raster=*/thread_task_runner,
+                               /*ui=*/thread_task_runner,
+                               /*io=*/thread_task_runner);
+  auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
+  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
+      /*delegate=*/mock_delegate,
+      /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
+      /*platform_views_controller=*/flutterPlatformViewsController,
+      /*task_runners=*/runners);
+
+  UIView* mockFlutterView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)] autorelease];
+  flutterPlatformViewsController->SetFlutterView(mockFlutterView);
+
+  FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+      [[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
+  flutterPlatformViewsController->RegisterViewFactory(
+      factory, @"MockFlutterPlatformView",
+      FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
+  FlutterResult result = ^(id result) {
+  };
+
+  flutterPlatformViewsController->OnMethodCall(
+      [FlutterMethodCall
+          methodCallWithMethodName:@"create"
+                         arguments:@{@"id" : @0, @"viewType" : @"MockFlutterPlatformView"}],
+      result);
+
+  // First frame, |GetCurrentCanvases| is not empty after composite.
+  flutterPlatformViewsController->BeginFrame(SkISize::Make(300, 300));
+  flutter::MutatorsStack stack;
+  SkMatrix finalMatrix;
+  auto embeddedViewParams1 =
+      std::make_unique<flutter::EmbeddedViewParams>(finalMatrix, SkSize::Make(300, 300), stack);
+  flutterPlatformViewsController->PrerollCompositeEmbeddedView(0, std::move(embeddedViewParams1));
+  flutterPlatformViewsController->CompositeEmbeddedView(0);
+  XCTAssertEqual(flutterPlatformViewsController->GetCurrentCanvases().size(), 1UL);
+
+  // Second frame, |GetCurrentCanvases| should be empty at the start
+  flutterPlatformViewsController->BeginFrame(SkISize::Make(300, 300));
+  XCTAssertTrue(flutterPlatformViewsController->GetCurrentCanvases().empty());
+
+  auto embeddedViewParams2 =
+      std::make_unique<flutter::EmbeddedViewParams>(finalMatrix, SkSize::Make(300, 300), stack);
+  flutterPlatformViewsController->PrerollCompositeEmbeddedView(0, std::move(embeddedViewParams2));
+  flutterPlatformViewsController->CompositeEmbeddedView(0);
+  XCTAssertEqual(flutterPlatformViewsController->GetCurrentCanvases().size(), 1UL);
+}
+
 - (int)alphaOfPoint:(CGPoint)point onView:(UIView*)view {
   unsigned char pixel[4] = {0};
 
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
index 4192562..584a201 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h
@@ -141,7 +141,8 @@
       NSString* factoryId,
       FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy);
 
-  void SetFrameSize(SkISize frame_size);
+  // Called at the begining of each frame.
+  void BeginFrame(SkISize frame_size);
 
   // Indicates that we don't compisite any platform views or overlays during this frame.
   // Also reverts the composition_order_ to its original state at the begining of the frame.
@@ -175,12 +176,6 @@
                    std::unique_ptr<SurfaceFrame> frame,
                    const std::shared_ptr<fml::SyncSwitch>& gpu_disable_sync_switch);
 
-  // Invoked at the very end of a frame.
-  // After invoking this method, nothing should happen on the current TaskRunner during the same
-  // frame.
-  void EndFrame(bool should_resubmit_frame,
-                fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger);
-
   void OnMethodCall(FlutterMethodCall* call, FlutterResult& result);
 
  private:
@@ -309,6 +304,9 @@
                           std::shared_ptr<IOSContext> ios_context,
                           std::unique_ptr<SurfaceFrame> frame);
 
+  // Resets the state of the frame.
+  void ResetFrameState();
+
   FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewsController);
 };
 
diff --git a/shell/platform/darwin/ios/ios_external_view_embedder.mm b/shell/platform/darwin/ios/ios_external_view_embedder.mm
index cfb3424..9ea32d1 100644
--- a/shell/platform/darwin/ios/ios_external_view_embedder.mm
+++ b/shell/platform/darwin/ios/ios_external_view_embedder.mm
@@ -37,7 +37,7 @@
     fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
   TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::BeginFrame");
   FML_CHECK(platform_views_controller_);
-  platform_views_controller_->SetFrameSize(frame_size);
+  platform_views_controller_->BeginFrame(frame_size);
 }
 
 // |ExternalViewEmbedder|
@@ -88,7 +88,6 @@
                                        fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
   TRACE_EVENT0("flutter", "IOSExternalViewEmbedder::EndFrame");
   FML_CHECK(platform_views_controller_);
-  return platform_views_controller_->EndFrame(should_resubmit_frame, raster_thread_merger);
 }
 
 // |ExternalViewEmbedder|
diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj
index 98565ef..c52019d 100644
--- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj
+++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj
@@ -57,6 +57,7 @@
 		6816DBAD2318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */; };
 		6816DBAE2318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */; };
 		68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */; };
+		68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 68D4017C2564859300ECD91A /* ContinuousTexture.m */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -172,6 +173,8 @@
 		6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_iPhone SE_simulator.png"; sourceTree = "<group>"; };
 		6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_iPhone SE_simulator.png"; sourceTree = "<group>"; };
 		68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGestureRecognizerTests.m; sourceTree = "<group>"; };
+		68D4017B2564859300ECD91A /* ContinuousTexture.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContinuousTexture.h; sourceTree = "<group>"; };
+		68D4017C2564859300ECD91A /* ContinuousTexture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContinuousTexture.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -238,6 +241,8 @@
 				0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */,
 				0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */,
 				0A57B3C02323C74D00DD9521 /* FlutterEngine+ScenariosTest.h */,
+				68D4017B2564859300ECD91A /* ContinuousTexture.h */,
+				68D4017C2564859300ECD91A /* ContinuousTexture.m */,
 			);
 			path = Scenarios;
 			sourceTree = "<group>";
@@ -462,6 +467,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				248D76DA22E388380012F0C1 /* main.m in Sources */,
+				68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */,
 				24F1FB89230B4579005ACE7C /* TextPlatformView.m in Sources */,
 				248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */,
 				0A57B3BF2323C74200DD9521 /* FlutterEngine+ScenariosTest.m in Sources */,
diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
index 96ddd0c..0cbdafa 100644
--- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
+++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
@@ -4,6 +4,7 @@
 
 #import "AppDelegate.h"
 
+#import "ContinuousTexture.h"
 #import "FlutterEngine+ScenariosTest.h"
 #import "ScreenBeforeFlutter.h"
 #import "TextPlatformView.h"
@@ -52,6 +53,7 @@
     @"--tap-status-bar" : @"tap_status_bar",
     @"--text-semantics-focus" : @"text_semantics_focus",
     @"--animated-color-square" : @"animated_color_square",
+    @"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture"
   };
   __block NSString* flutterViewControllerTestName = nil;
   [launchArgsMap
@@ -70,6 +72,10 @@
   }
 
   [self.window makeKeyAndVisible];
+  if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--with-continuous-texture"]) {
+    [ContinuousTexture
+        registerWithRegistrar:[self registrarForPlugin:@"com.constant.firing.texture"]];
+  }
   return [super application:application didFinishLaunchingWithOptions:launchOptions];
 }
 
diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/ContinuousTexture.h b/testing/scenario_app/ios/Scenarios/Scenarios/ContinuousTexture.h
new file mode 100644
index 0000000..996de85
--- /dev/null
+++ b/testing/scenario_app/ios/Scenarios/Scenarios/ContinuousTexture.h
@@ -0,0 +1,20 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Flutter/Flutter.h>
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// A texture plugin that ready textures continuously.
+@interface ContinuousTexture : NSObject <FlutterPlugin>
+
+@end
+
+// The testing texture used by |ContinuousTexture|
+@interface FlutterScenarioTestTexture : NSObject <FlutterTexture>
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/ContinuousTexture.m b/testing/scenario_app/ios/Scenarios/Scenarios/ContinuousTexture.m
new file mode 100644
index 0000000..0211062
--- /dev/null
+++ b/testing/scenario_app/ios/Scenarios/Scenarios/ContinuousTexture.m
@@ -0,0 +1,43 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ContinuousTexture.h"
+
+@implementation ContinuousTexture
+
++ (void)registerWithRegistrar:(nonnull NSObject<FlutterPluginRegistrar>*)registrar {
+  NSObject<FlutterTextureRegistry>* textureRegistry = [registrar textures];
+  FlutterScenarioTestTexture* texture = [[FlutterScenarioTestTexture alloc] init];
+  int64_t textureId = [textureRegistry registerTexture:texture];
+  [NSTimer scheduledTimerWithTimeInterval:0.05
+                                  repeats:YES
+                                    block:^(NSTimer* _Nonnull timer) {
+                                      [textureRegistry textureFrameAvailable:textureId];
+                                    }];
+}
+
+@end
+
+@implementation FlutterScenarioTestTexture
+
+- (CVPixelBufferRef _Nullable)copyPixelBuffer {
+  return [self pixelBuffer];
+}
+
+- (CVPixelBufferRef)pixelBuffer {
+  NSDictionary* options = @{
+    // This key is required to generate SKPicture with CVPixelBufferRef in metal.
+    (NSString*)kCVPixelBufferMetalCompatibilityKey : @YES
+  };
+  CVPixelBufferRef pxbuffer = NULL;
+  CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, 200, 200, kCVPixelFormatType_32BGRA,
+                                        (__bridge CFDictionaryRef)options, &pxbuffer);
+
+  NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
+
+  CVPixelBufferLockBaseAddress(pxbuffer, 0);
+  return pxbuffer;
+}
+
+@end
diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m
index 1d23e3a..24419a6 100644
--- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m
+++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m
@@ -4,6 +4,8 @@
 
 #import "GoldenPlatformViewTests.h"
 
+static const NSInteger kSecondsToWaitForPlatformView = 30;
+
 @interface PlatformViewUITests : GoldenPlatformViewTests
 
 @end
@@ -170,4 +172,35 @@
   XCUIDevice.sharedDevice.orientation = UIDeviceOrientationLandscapeLeft;
   [self checkGolden];
 }
+
+@end
+
+@interface PlatformViewWithContinuousTexture : XCTestCase
+
+@end
+
+@implementation PlatformViewWithContinuousTexture
+
+- (void)setUp {
+  self.continueAfterFailure = NO;
+}
+
+- (void)testPlatformViewWithContinuousTexture {
+  XCUIApplication* app = [[XCUIApplication alloc] init];
+  app.launchArguments =
+      @[ @"--platform-view-with-continuous-texture", @"--with-continuous-texture" ];
+  [app launch];
+
+  XCUIElement* platformView = app.textViews.firstMatch;
+  BOOL exists = [platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView];
+  if (!exists) {
+    XCTFail(@"It took longer than %@ second to find the platform view."
+            @"There might be issues with the platform view's construction,"
+            @"or with how the scenario is built.",
+            @(kSecondsToWaitForPlatformView));
+  }
+
+  XCTAssertNotNil(platformView);
+}
+
 @end
diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart
index fd6f5f4..31f45fd 100644
--- a/testing/scenario_app/lib/src/platform_view.dart
+++ b/testing/scenario_app/lib/src/platform_view.dart
@@ -613,6 +613,23 @@
   }
 }
 
+/// A simple platform view for testing platform view with a continuous texture layer.
+/// For example, it simulates a video being played.
+class PlatformViewWithContinuousTexture extends PlatformViewScenario {
+  /// Constructs a platform view with continuous texture layer.
+  PlatformViewWithContinuousTexture(PlatformDispatcher dispatcher, String text, { int id = 0 })
+      : super(dispatcher, text, id: id);
+
+  @override
+  void onBeginFrame(Duration duration) {
+    final SceneBuilder builder = SceneBuilder();
+
+    builder.addTexture(0, width: 300, height: 300, offset: const Offset(200, 200));
+
+    finishBuilderByAddingPlatformViewAndPicture(builder, id);
+  }
+}
+
 mixin _BasePlatformViewScenarioMixin on Scenario {
   int _textureId;
 
diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart
index 7431752..e2c1f27 100644
--- a/testing/scenario_app/lib/src/scenarios.dart
+++ b/testing/scenario_app/lib/src/scenarios.dart
@@ -43,6 +43,7 @@
   'tap_status_bar': () => TouchesScenario(PlatformDispatcher.instance),
   'text_semantics_focus': () => SendTextFocusSemantics(PlatformDispatcher.instance),
   'initial_route_reply': () => InitialRouteReply(PlatformDispatcher.instance),
+  'platform_view_with_continuous_texture': () => PlatformViewWithContinuousTexture(PlatformDispatcher.instance, 'Platform View', id: _viewId++),
 };
 
 Map<String, dynamic> _currentScenarioParams = <String, dynamic>{};