diff --git a/.ci.yaml b/.ci.yaml index 10323f566d916..13c8ba1f7e419 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -387,7 +387,6 @@ targets: - os=Mac-13 - name: Mac mac_unopt - bringup: true recipe: engine_v2/engine_v2 properties: config_name: mac_unopt diff --git a/DEPS b/DEPS index 12da47b50825b..2b582f128c672 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'b9c16065b76d1d80940d8239199414d19bcf73e8', + 'skia_revision': 'dd8cd405d14575917d7b941cde0a7dac1f00b5f6', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. diff --git a/ci/builders/windows_android_aot_engine.json b/ci/builders/windows_android_aot_engine.json index 933478b3523b8..a35dad5ea2777 100644 --- a/ci/builders/windows_android_aot_engine.json +++ b/ci/builders/windows_android_aot_engine.json @@ -16,10 +16,15 @@ "device_type=none", "os=Windows-10" ], + "gclient_variables": { + "use_rbe": true + }, "gn": [ "--runtime-mode", "profile", - "--android" + "--android", + "--no-goma", + "--rbe" ], "name": "android_profile", "ninja": { @@ -45,11 +50,16 @@ "device_type=none", "os=Windows-10" ], + "gclient_variables": { + "use_rbe": true + }, "gn": [ "--runtime-mode", "profile", "--android", - "--android-cpu=arm64" + "--android-cpu=arm64", + "--no-goma", + "--rbe" ], "name": "android_profile_arm64", "ninja": { @@ -75,11 +85,16 @@ "device_type=none", "os=Windows-10" ], + "gclient_variables": { + "use_rbe": true + }, "gn": [ "--runtime-mode", "profile", "--android", - "--android-cpu=x64" + "--android-cpu=x64", + "--no-goma", + "--rbe" ], "name": "android_profile_x64", "ninja": { @@ -105,10 +120,15 @@ "device_type=none", "os=Windows-10" ], + "gclient_variables": { + "use_rbe": true + }, "gn": [ "--runtime-mode", "release", - "--android" + "--android", + "--no-goma", + "--rbe" ], "name": "android_release", "ninja": { @@ -134,11 +154,16 @@ "device_type=none", "os=Windows-10" ], + "gclient_variables": { + "use_rbe": true + }, "gn": [ "--runtime-mode", "release", "--android", - "--android-cpu=arm64" + "--android-cpu=arm64", + "--no-goma", + "--rbe" ], "name": "android_release_arm64", "ninja": { @@ -164,11 +189,16 @@ "device_type=none", "os=Windows-10" ], + "gclient_variables": { + "use_rbe": true + }, "gn": [ "--runtime-mode", "release", "--android", - "--android-cpu=x64" + "--android-cpu=x64", + "--no-goma", + "--rbe" ], "name": "android_release_x64", "ninja": { diff --git a/ci/builders/windows_arm_host_engine.json b/ci/builders/windows_arm_host_engine.json index 406533a6d6fa2..8912dade7ef74 100644 --- a/ci/builders/windows_arm_host_engine.json +++ b/ci/builders/windows_arm_host_engine.json @@ -22,14 +22,17 @@ "os=Windows-10" ], "gclient_variables": { - "download_android_deps": false + "download_android_deps": false, + "use_rbe": true }, "gn": [ "--runtime-mode", "debug", "--no-lto", "--windows-cpu", - "arm64" + "arm64", + "--no-goma", + "--rbe" ], "name": "host_debug_arm64", "ninja": { @@ -61,14 +64,17 @@ "os=Windows-10" ], "gclient_variables": { - "download_android_deps": false + "download_android_deps": false, + "use_rbe": true }, "gn": [ "--runtime-mode", "profile", "--no-lto", "--windows-cpu", - "arm64" + "arm64", + "--no-goma", + "--rbe" ], "name": "host_profile_arm64", "ninja": { @@ -97,7 +103,8 @@ "os=Windows-10" ], "gclient_variables": { - "download_android_deps": false + "download_android_deps": false, + "use_rbe": true }, "generators": {}, "gn": [ @@ -105,7 +112,9 @@ "release", "--no-lto", "--windows-cpu", - "arm64" + "arm64", + "--no-goma", + "--rbe" ], "name": "host_release_arm64", "ninja": { diff --git a/ci/builders/windows_host_engine.json b/ci/builders/windows_host_engine.json index 1a9903e22cb62..bca555a9f4499 100644 --- a/ci/builders/windows_host_engine.json +++ b/ci/builders/windows_host_engine.json @@ -22,12 +22,15 @@ "os=Windows-10" ], "gclient_variables": { - "download_android_deps": false + "download_android_deps": false, + "use_rbe": true }, "gn": [ "--runtime-mode", "debug", - "--no-lto" + "--no-lto", + "--no-goma", + "--rbe" ], "name": "host_debug", "ninja": { @@ -73,12 +76,15 @@ "os=Windows-10" ], "gclient_variables": { - "download_android_deps": false + "download_android_deps": false, + "use_rbe": true }, "gn": [ "--runtime-mode", "profile", - "--no-lto" + "--no-lto", + "--no-goma", + "--rbe" ], "name": "host_profile", "ninja": { @@ -107,13 +113,16 @@ "os=Windows-10" ], "gclient_variables": { - "download_android_deps": false + "download_android_deps": false, + "use_rbe": true }, "generators": {}, "gn": [ "--runtime-mode", "release", - "--no-lto" + "--no-lto", + "--no-goma", + "--rbe" ], "name": "host_release", "ninja": { diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index d287ed8733f88..2d40298c8be5d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -10324,6 +10324,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart + ../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart + ../../../flutter/LICENSE @@ -13161,6 +13162,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart diff --git a/impeller/geometry/geometry_benchmarks.cc b/impeller/geometry/geometry_benchmarks.cc index 484744ad05258..98d17cf7d8c15 100644 --- a/impeller/geometry/geometry_benchmarks.cc +++ b/impeller/geometry/geometry_benchmarks.cc @@ -97,20 +97,28 @@ static void BM_Polyline(benchmark::State& state, Args&&... args) { state.counters["TotalPointCount"] = point_count; } +enum class UVMode { + kNoUV, + kUVRect, + kUVRectTx, +}; + template static void BM_StrokePolyline(benchmark::State& state, Args&&... args) { auto args_tuple = std::make_tuple(std::move(args)...); auto path = std::get(args_tuple); auto cap = std::get(args_tuple); auto join = std::get(args_tuple); - auto generate_uv = std::get(args_tuple); + auto generate_uv = std::get(args_tuple); const Scalar stroke_width = 5.0f; const Scalar miter_limit = 10.0f; const Scalar scale = 1.0f; const Point texture_origin = Point(0, 0); const Size texture_size = Size(100, 100); - const Matrix effect_transform = Matrix::MakeScale({2.0f, 2.0f, 1.0f}); + const Matrix effect_transform = (generate_uv == UVMode::kUVRectTx) + ? Matrix::MakeScale({2.0f, 2.0f, 1.0f}) + : Matrix(); auto points = std::make_unique>(); points->reserve(2048); @@ -123,15 +131,15 @@ static void BM_StrokePolyline(benchmark::State& state, Args&&... args) { size_t point_count = 0u; size_t single_point_count = 0u; while (state.KeepRunning()) { - if (generate_uv) { + if (generate_uv == UVMode::kNoUV) { + auto vertices = ImpellerBenchmarkAccessor::GenerateSolidStrokeVertices( + polyline, stroke_width, miter_limit, join, cap, scale); + single_point_count = vertices.size(); + } else { auto vertices = ImpellerBenchmarkAccessor::GenerateSolidStrokeVerticesUV( polyline, stroke_width, miter_limit, join, cap, scale, // texture_origin, texture_size, effect_transform); single_point_count = vertices.size(); - } else { - auto vertices = ImpellerBenchmarkAccessor::GenerateSolidStrokeVertices( - polyline, stroke_width, miter_limit, join, cap, scale); - single_point_count = vertices.size(); } point_count += single_point_count; } @@ -157,24 +165,21 @@ static void BM_Convex(benchmark::State& state, Args&&... args) { state.counters["TotalPointCount"] = point_count; } -#define MAKE_STROKE_BENCHMARK_CAPTURE(path, cap, join, closed) \ - BENCHMARK_CAPTURE(BM_StrokePolyline, stroke_##path##_##cap##_##join, \ - Create##path(closed), Cap::k##cap, Join::k##join, false) - -#define MAKE_STROKE_BENCHMARK_CAPTURE_UV(path, cap, join, closed) \ - BENCHMARK_CAPTURE(BM_StrokePolyline, stroke_##path##_##cap##_##join##_uv, \ - Create##path(closed), Cap::k##cap, Join::k##join, true) +#define MAKE_STROKE_BENCHMARK_CAPTURE(path, cap, join, closed, uvname, uvtype) \ + BENCHMARK_CAPTURE(BM_StrokePolyline, stroke_##path##_##cap##_##join##uvname, \ + Create##path(closed), Cap::k##cap, Join::k##join, uvtype) -#define MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, uv) \ - MAKE_STROKE_BENCHMARK_CAPTURE##uv(path, Butt, Bevel, false); \ - MAKE_STROKE_BENCHMARK_CAPTURE##uv(path, Butt, Miter, false); \ - MAKE_STROKE_BENCHMARK_CAPTURE##uv(path, Butt, Round, false); \ - MAKE_STROKE_BENCHMARK_CAPTURE##uv(path, Square, Bevel, false); \ - MAKE_STROKE_BENCHMARK_CAPTURE##uv(path, Round, Bevel, false) +#define MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, uvname, uvtype) \ + MAKE_STROKE_BENCHMARK_CAPTURE(path, Butt, Bevel, false, uvname, uvtype); \ + MAKE_STROKE_BENCHMARK_CAPTURE(path, Butt, Miter, false, uvname, uvtype); \ + MAKE_STROKE_BENCHMARK_CAPTURE(path, Butt, Round, false, uvname, uvtype); \ + MAKE_STROKE_BENCHMARK_CAPTURE(path, Square, Bevel, false, uvname, uvtype); \ + MAKE_STROKE_BENCHMARK_CAPTURE(path, Round, Bevel, false, uvname, uvtype) -#define MAKE_STROKE_BENCHMARK_CAPTURE_UVS(path) \ - MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, ); \ - MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, _UV) +#define MAKE_STROKE_BENCHMARK_CAPTURE_UVS(path) \ + MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, , UVMode::kNoUV); \ + MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, _uv, UVMode::kUVRectTx); \ + MAKE_STROKE_BENCHMARK_CAPTURE_CAPS_JOINS(path, _uvNoTx, UVMode::kUVRect) BENCHMARK_CAPTURE(BM_Polyline, cubic_polyline, CreateCubic(true), false); BENCHMARK_CAPTURE(BM_Polyline, cubic_polyline_tess, CreateCubic(true), true); @@ -201,8 +206,9 @@ BENCHMARK_CAPTURE(BM_Polyline, MAKE_STROKE_BENCHMARK_CAPTURE_UVS(Quadratic); BENCHMARK_CAPTURE(BM_Convex, rrect_convex, CreateRRect(), true); -MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Bevel, ); -MAKE_STROKE_BENCHMARK_CAPTURE_UV(RRect, Butt, Bevel, ); +MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Bevel, , , UVMode::kNoUV); +MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Bevel, , _uv, UVMode::kUVRectTx); +MAKE_STROKE_BENCHMARK_CAPTURE(RRect, Butt, Bevel, , _uvNoTx, UVMode::kUVRect); namespace { diff --git a/impeller/renderer/backend/vulkan/test/mock_vulkan.cc b/impeller/renderer/backend/vulkan/test/mock_vulkan.cc index c3bc7fbf72186..72a36804bda88 100644 --- a/impeller/renderer/backend/vulkan/test/mock_vulkan.cc +++ b/impeller/renderer/backend/vulkan/test/mock_vulkan.cc @@ -42,8 +42,6 @@ struct MockImage {}; struct MockSemaphore {}; -struct MockFramebuffer {}; - static ISize currentImageSize = ISize{1, 1}; class MockDevice final { @@ -688,14 +686,6 @@ VkResult vkAcquireNextImageKHR(VkDevice device, return VK_SUCCESS; } -VkResult vkCreateFramebuffer(VkDevice device, - const VkFramebufferCreateInfo* pCreateInfo, - const VkAllocationCallbacks* pAllocator, - VkFramebuffer* pFramebuffer) { - *pFramebuffer = reinterpret_cast(new MockFramebuffer()); - return VK_SUCCESS; -} - PFN_vkVoidFunction GetMockVulkanProcAddress(VkInstance instance, const char* pName) { if (strcmp("vkEnumerateInstanceExtensionProperties", pName) == 0) { @@ -824,8 +814,6 @@ PFN_vkVoidFunction GetMockVulkanProcAddress(VkInstance instance, return (PFN_vkVoidFunction)vkDestroySurfaceKHR; } else if (strcmp("vkAcquireNextImageKHR", pName) == 0) { return (PFN_vkVoidFunction)vkAcquireNextImageKHR; - } else if (strcmp("vkCreateFramebuffer", pName) == 0) { - return (PFN_vkVoidFunction)vkCreateFramebuffer; } return noop; } diff --git a/impeller/renderer/backend/vulkan/test/swapchain_unittests.cc b/impeller/renderer/backend/vulkan/test/swapchain_unittests.cc index f0af23ed75b46..a78d3187b58df 100644 --- a/impeller/renderer/backend/vulkan/test/swapchain_unittests.cc +++ b/impeller/renderer/backend/vulkan/test/swapchain_unittests.cc @@ -6,7 +6,6 @@ #include "gtest/gtest.h" #include "impeller/renderer/backend/vulkan/swapchain_vk.h" #include "impeller/renderer/backend/vulkan/test/mock_vulkan.h" -#include "impeller/renderer/backend/vulkan/texture_vk.h" #include "vulkan/vulkan_enums.hpp" namespace impeller { @@ -53,43 +52,5 @@ TEST(SwapchainTest, RecreateSwapchainWhenSizeChanges) { EXPECT_EQ(image_b->GetSize(), expected_size); } -TEST(SwapchainTest, CachesRenderPassOnSwapchainImage) { - auto const context = MockVulkanContextBuilder().Build(); - - auto surface = CreateSurface(*context); - auto swapchain = - SwapchainVK::Create(context, std::move(surface), ISize{1, 1}); - - EXPECT_TRUE(swapchain->IsValid()); - - // We should create 3 swapchain images with the current mock setup. However, - // we will only ever return the first image, so the render pass and - // framebuffer will be cached after one call to AcquireNextDrawable. - auto drawable = swapchain->AcquireNextDrawable(); - RenderTarget render_target = drawable->GetTargetRenderPassDescriptor(); - - auto texture = render_target.GetRenderTargetTexture(); - auto& texture_vk = TextureVK::Cast(*texture); - EXPECT_EQ(texture_vk.GetFramebuffer(), nullptr); - EXPECT_EQ(texture_vk.GetRenderPass(), nullptr); - - auto command_buffer = context->CreateCommandBuffer(); - auto render_pass = command_buffer->CreateRenderPass(render_target); - - render_pass->EncodeCommands(); - - EXPECT_NE(texture_vk.GetFramebuffer(), nullptr); - EXPECT_NE(texture_vk.GetRenderPass(), nullptr); - - { - auto drawable = swapchain->AcquireNextDrawable(); - auto texture = render_target.GetRenderTargetTexture(); - auto& texture_vk = TextureVK::Cast(*texture); - - EXPECT_NE(texture_vk.GetFramebuffer(), nullptr); - EXPECT_NE(texture_vk.GetRenderPass(), nullptr); - } -} - } // namespace testing } // namespace impeller diff --git a/impeller/renderer/backend/vulkan/texture_source_vk.cc b/impeller/renderer/backend/vulkan/texture_source_vk.cc index f200bc5778aaf..4a39bce151bd8 100644 --- a/impeller/renderer/backend/vulkan/texture_source_vk.cc +++ b/impeller/renderer/backend/vulkan/texture_source_vk.cc @@ -58,22 +58,4 @@ fml::Status TextureSourceVK::SetLayout(const BarrierVK& barrier) const { return {}; } -void TextureSourceVK::SetFramebuffer( - const SharedHandleVK& framebuffer) { - framebuffer_ = framebuffer; -} - -void TextureSourceVK::SetRenderPass( - const SharedHandleVK& render_pass) { - render_pass_ = render_pass; -} - -SharedHandleVK TextureSourceVK::GetFramebuffer() const { - return framebuffer_; -} - -SharedHandleVK TextureSourceVK::GetRenderPass() const { - return render_pass_; -} - } // namespace impeller diff --git a/impeller/renderer/backend/vulkan/texture_source_vk.h b/impeller/renderer/backend/vulkan/texture_source_vk.h index 502a9a180dfcf..4f76a8067cf4f 100644 --- a/impeller/renderer/backend/vulkan/texture_source_vk.h +++ b/impeller/renderer/backend/vulkan/texture_source_vk.h @@ -65,38 +65,12 @@ class TextureSourceVK { /// Whether or not this is a swapchain image. virtual bool IsSwapchainImage() const = 0; - /// Store the last framebuffer object used with this texture. - /// - /// This field is only set if this texture is used as the resolve texture - /// of a render pass. By construction, this framebuffer should be compatible - /// with any future render passes. - void SetFramebuffer(const SharedHandleVK& framebuffer); - - /// Store the last render pass object used with this texture. - /// - /// This field is only set if this texture is used as the resolve texture - /// of a render pass. By construction, this framebuffer should be compatible - /// with any future render passes. - void SetRenderPass(const SharedHandleVK& render_pass); - - /// Retrieve the last framebuffer object used with this texture. - /// - /// May be nullptr if no previous framebuffer existed. - SharedHandleVK GetFramebuffer() const; - - /// Retrieve the last render pass object used with this texture. - /// - /// May be nullptr if no previous render pass existed. - SharedHandleVK GetRenderPass() const; - protected: const TextureDescriptor desc_; explicit TextureSourceVK(TextureDescriptor desc); private: - SharedHandleVK framebuffer_ = nullptr; - SharedHandleVK render_pass_ = nullptr; mutable RWMutex layout_mutex_; mutable vk::ImageLayout layout_ IPLR_GUARDED_BY(layout_mutex_) = vk::ImageLayout::eUndefined; diff --git a/impeller/renderer/backend/vulkan/texture_vk.cc b/impeller/renderer/backend/vulkan/texture_vk.cc index cd3d93ff7fb0d..b105d31b96e85 100644 --- a/impeller/renderer/backend/vulkan/texture_vk.cc +++ b/impeller/renderer/backend/vulkan/texture_vk.cc @@ -175,20 +175,20 @@ vk::ImageView TextureVK::GetRenderTargetView() const { void TextureVK::SetFramebuffer( const SharedHandleVK& framebuffer) { - source_->SetFramebuffer(framebuffer); + framebuffer_ = framebuffer; } void TextureVK::SetRenderPass( const SharedHandleVK& render_pass) { - source_->SetRenderPass(render_pass); + render_pass_ = render_pass; } SharedHandleVK TextureVK::GetFramebuffer() const { - return source_->GetFramebuffer(); + return framebuffer_; } SharedHandleVK TextureVK::GetRenderPass() const { - return source_->GetRenderPass(); + return render_pass_; } } // namespace impeller diff --git a/impeller/renderer/backend/vulkan/texture_vk.h b/impeller/renderer/backend/vulkan/texture_vk.h index 7a8be812eab31..5826e3df78174 100644 --- a/impeller/renderer/backend/vulkan/texture_vk.h +++ b/impeller/renderer/backend/vulkan/texture_vk.h @@ -73,6 +73,8 @@ class TextureVK final : public Texture, public BackendCast { private: std::weak_ptr context_; std::shared_ptr source_; + SharedHandleVK framebuffer_ = nullptr; + SharedHandleVK render_pass_ = nullptr; // |Texture| void SetLabel(std::string_view label) override; diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 9697966fe3bf0..0fbda333aac87 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -65,6 +65,7 @@ export 'engine/font_fallback_data.dart'; export 'engine/font_fallbacks.dart'; export 'engine/fonts.dart'; export 'engine/frame_reference.dart'; +export 'engine/frame_timing_recorder.dart'; export 'engine/html/backdrop_filter.dart'; export 'engine/html/bitmap_canvas.dart'; export 'engine/html/canvas.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 58a23e871cc92..bdf6d744f7152 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -131,6 +131,7 @@ abstract class DisplayCanvas { typedef RenderRequest = ({ ui.Scene scene, Completer completer, + FrameTimingRecorder? recorder, }); /// A per-view queue of render requests. Only contains the current render diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index f4fdaef67c0f9..7d673a63c6b3e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -417,16 +417,17 @@ class CanvasKitRenderer implements Renderer { "Unable to render to a view which hasn't been registered"); final ViewRasterizer rasterizer = _rasterizers[view.viewId]!; final RenderQueue renderQueue = rasterizer.queue; + final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; if (renderQueue.current != null) { // If a scene is already queued up, drop it and queue this one up instead // so that the scene view always displays the most recently requested scene. renderQueue.next?.completer.complete(); final Completer completer = Completer(); - renderQueue.next = (scene: scene, completer: completer); + renderQueue.next = (scene: scene, completer: completer, recorder: recorder); return completer.future; } final Completer completer = Completer(); - renderQueue.current = (scene: scene, completer: completer); + renderQueue.current = (scene: scene, completer: completer, recorder: recorder); unawaited(_kickRenderLoop(rasterizer)); return completer.future; } @@ -435,7 +436,7 @@ class CanvasKitRenderer implements Renderer { final RenderQueue renderQueue = rasterizer.queue; final RenderRequest current = renderQueue.current!; try { - await _renderScene(current.scene, rasterizer); + await _renderScene(current.scene, rasterizer, current.recorder); current.completer.complete(); } catch (error, stackTrace) { current.completer.completeError(error, stackTrace); @@ -449,7 +450,7 @@ class CanvasKitRenderer implements Renderer { } } - Future _renderScene(ui.Scene scene, ViewRasterizer rasterizer) async { + Future _renderScene(ui.Scene scene, ViewRasterizer rasterizer, FrameTimingRecorder? recorder) async { // "Build finish" and "raster start" happen back-to-back because we // render on the same thread, so there's no overhead from hopping to // another thread. @@ -457,11 +458,12 @@ class CanvasKitRenderer implements Renderer { // CanvasKit works differently from the HTML renderer in that in HTML // we update the DOM in SceneBuilder.build, which is these function calls // here are CanvasKit-only. - frameTimingsOnBuildFinish(); - frameTimingsOnRasterStart(); + recorder?.recordBuildFinish(); + recorder?.recordRasterStart(); await rasterizer.draw((scene as LayerScene).layerTree); - frameTimingsOnRasterFinish(); + recorder?.recordRasterFinish(); + recorder?.submitTimings(); } // Map from view id to the associated Rasterizer for that view. diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 750587774c313..386666d6797ae 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -1487,7 +1487,7 @@ class DomCanvasRenderingContextBitmapRenderer {} extension DomCanvasRenderingContextBitmapRendererExtension on DomCanvasRenderingContextBitmapRenderer { - external void transferFromImageBitmap(DomImageBitmap bitmap); + external void transferFromImageBitmap(DomImageBitmap? bitmap); } @JS('ImageData') diff --git a/lib/web_ui/lib/src/engine/frame_timing_recorder.dart b/lib/web_ui/lib/src/engine/frame_timing_recorder.dart new file mode 100644 index 0000000000000..ec2944bb41327 --- /dev/null +++ b/lib/web_ui/lib/src/engine/frame_timing_recorder.dart @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +class FrameTimingRecorder { + final int _vsyncStartMicros = _currentFrameVsyncStart; + final int _buildStartMicros = _currentFrameBuildStart; + + int? _buildFinishMicros; + int? _rasterStartMicros; + int? _rasterFinishMicros; + + /// Collects frame timings from frames. + /// + /// This list is periodically reported to the framework (see [_kFrameTimingsSubmitInterval]). + static List _frameTimings = []; + + /// These two metrics are collected early in the process, before the respective + /// scene builders are created. These are instead treated as global state, which + /// are used to initialize any recorders that are created by the scene builders. + static int _currentFrameVsyncStart = 0; + static int _currentFrameBuildStart = 0; + + static void recordCurrentFrameVsync() { + if (frameTimingsEnabled) { + _currentFrameVsyncStart = _nowMicros(); + } + } + + static void recordCurrentFrameBuildStart() { + if (frameTimingsEnabled) { + _currentFrameBuildStart = _nowMicros(); + } + } + + /// The last time (in microseconds) we submitted frame timings. + static int _frameTimingsLastSubmitTime = _nowMicros(); + /// The amount of time in microseconds we wait between submitting + /// frame timings. + static const int _kFrameTimingsSubmitInterval = 100000; // 100 milliseconds + + /// Whether we are collecting [ui.FrameTiming]s. + static bool get frameTimingsEnabled { + return EnginePlatformDispatcher.instance.onReportTimings != null; + } + + /// Current timestamp in microseconds taken from the high-precision + /// monotonically increasing timer. + /// + /// See also: + /// + /// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now, + /// particularly notes about Firefox rounding to 1ms for security reasons, + /// which can be bypassed in tests by setting certain browser options. + static int _nowMicros() { + return (domWindow.performance.now() * 1000).toInt(); + } + + void recordBuildFinish([int? buildFinish]) { + assert(_buildFinishMicros == null, "can't record build finish more than once"); + _buildFinishMicros = buildFinish ?? _nowMicros(); + } + + void recordRasterStart([int? rasterStart]) { + assert(_rasterStartMicros == null, "can't record raster start more than once"); + _rasterStartMicros = rasterStart ?? _nowMicros(); + } + + void recordRasterFinish([int? rasterFinish]) { + assert(_rasterFinishMicros == null, "can't record raster finish more than once"); + _rasterFinishMicros = rasterFinish ?? _nowMicros(); + } + + void submitTimings() { + assert( + _buildFinishMicros != null && + _rasterStartMicros != null && + _rasterFinishMicros != null, + 'Attempted to submit an incomplete timings.' + ); + final ui.FrameTiming timing = ui.FrameTiming( + vsyncStart: _vsyncStartMicros, + buildStart: _buildStartMicros, + buildFinish: _buildFinishMicros!, + rasterStart: _rasterStartMicros!, + rasterFinish: _rasterFinishMicros!, + rasterFinishWallTime: _rasterFinishMicros!, + ); + _frameTimings.add(timing); + final int now = _nowMicros(); + if (now - _frameTimingsLastSubmitTime > _kFrameTimingsSubmitInterval) { + _frameTimingsLastSubmitTime = now; + EnginePlatformDispatcher.instance.invokeOnReportTimings(_frameTimings); + _frameTimings = []; + } + } +} diff --git a/lib/web_ui/lib/src/engine/html/renderer.dart b/lib/web_ui/lib/src/engine/html/renderer.dart index c9febef391e37..b41fac3739234 100644 --- a/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/lib/web_ui/lib/src/engine/html/renderer.dart @@ -323,8 +323,11 @@ class HtmlRenderer implements Renderer { @override Future renderScene(ui.Scene scene, ui.FlutterView view) async { final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; - implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!); - frameTimingsOnRasterFinish(); + scene as SurfaceScene; + implicitView.dom.setScene(scene.webOnlyRootElement!); + final FrameTimingRecorder? recorder = scene.timingRecorder; + recorder?.recordRasterFinish(); + recorder?.submitTimings(); } @override diff --git a/lib/web_ui/lib/src/engine/html/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart index f15d043447e4b..b4deb9a6dac69 100644 --- a/lib/web_ui/lib/src/engine/html/scene.dart +++ b/lib/web_ui/lib/src/engine/html/scene.dart @@ -2,22 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:ui/src/engine/display.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../dom.dart'; -import '../vector_math.dart'; -import '../window.dart'; -import 'surface.dart'; - class SurfaceScene implements ui.Scene { /// This class is created by the engine, and should not be instantiated /// or extended directly. /// /// To create a Scene object, use a [SceneBuilder]. - SurfaceScene(this.webOnlyRootElement); + SurfaceScene(this.webOnlyRootElement, { + required this.timingRecorder, + }); final DomElement? webOnlyRootElement; + final FrameTimingRecorder? timingRecorder; /// Creates a raster image representation of the current state of the scene. /// This is a slow operation that is performed on a background thread. diff --git a/lib/web_ui/lib/src/engine/html/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart index e721aa5a5576b..701bb11ef92ad 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -7,7 +7,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; -import '../../engine.dart' show kProfileApplyFrame, kProfilePrerollFrame; +import '../../engine.dart' show FrameTimingRecorder, kProfileApplyFrame, kProfilePrerollFrame; import '../display.dart'; import '../dom.dart'; import '../picture.dart'; @@ -511,8 +511,9 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { // In the HTML renderer we time the beginning of the rasterization phase // (counter-intuitively) in SceneBuilder.build because DOM updates happen // here. This is different from CanvasKit. - frameTimingsOnBuildFinish(); - frameTimingsOnRasterStart(); + final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; + recorder?.recordBuildFinish(); + recorder?.recordRasterStart(); timeAction(kProfilePrerollFrame, () { while (_surfaceStack.length > 1) { // Auto-pop layers that were pushed without a corresponding pop. @@ -528,7 +529,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { } commitScene(_persistedScene); _lastFrameScene = _persistedScene; - return SurfaceScene(_persistedScene.rootElement); + return SurfaceScene(_persistedScene.rootElement, timingRecorder: recorder); }); } diff --git a/lib/web_ui/lib/src/engine/initialization.dart b/lib/web_ui/lib/src/engine/initialization.dart index 0dab016be43ba..745f8b2e84cf7 100644 --- a/lib/web_ui/lib/src/engine/initialization.dart +++ b/lib/web_ui/lib/src/engine/initialization.dart @@ -158,7 +158,15 @@ Future initializeEngineServices({ if (!waitingForAnimation) { waitingForAnimation = true; domWindow.requestAnimationFrame((JSNumber highResTime) { - frameTimingsOnVsync(); + FrameTimingRecorder.recordCurrentFrameVsync(); + + // In Flutter terminology "building a frame" consists of "beginning + // frame" and "drawing frame". + // + // We do not call `recordBuildFinish` from here because + // part of the rasterization process, particularly in the HTML + // renderer, takes place in the `SceneBuilder.build()`. + FrameTimingRecorder.recordCurrentFrameBuildStart(); // Reset immediately, because `frameHandler` can schedule more frames. waitingForAnimation = false; @@ -171,13 +179,6 @@ Future initializeEngineServices({ final int highResTimeMicroseconds = (1000 * highResTime.toDartDouble).toInt(); - // In Flutter terminology "building a frame" consists of "beginning - // frame" and "drawing frame". - // - // We do not call `frameTimingsOnBuildFinish` from here because - // part of the rasterization process, particularly in the HTML - // renderer, takes place in the `SceneBuilder.build()`. - frameTimingsOnBuildStart(); if (EnginePlatformDispatcher.instance.onBeginFrame != null) { EnginePlatformDispatcher.instance.invokeOnBeginFrame( Duration(microseconds: highResTimeMicroseconds)); diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart index ffabd12d2156b..d5ef8b3fa831b 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -5,11 +5,8 @@ import 'dart:async'; import 'dart:js_interop'; -import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; -import 'dom.dart'; -import 'platform_dispatcher.dart'; import 'util.dart'; // TODO(mdebbar): Deprecate this and remove it. @@ -127,118 +124,6 @@ class Profiler { } } -/// Whether we are collecting [ui.FrameTiming]s. -bool get _frameTimingsEnabled { - return EnginePlatformDispatcher.instance.onReportTimings != null; -} - -/// Collects frame timings from frames. -/// -/// This list is periodically reported to the framework (see -/// [_kFrameTimingsSubmitInterval]). -List _frameTimings = []; - -/// The amount of time in microseconds we wait between submitting -/// frame timings. -const int _kFrameTimingsSubmitInterval = 100000; // 100 milliseconds - -/// The last time (in microseconds) we submitted frame timings. -int _frameTimingsLastSubmitTime = _nowMicros(); - -// These variables store individual [ui.FrameTiming] properties. -int _vsyncStartMicros = -1; -int _buildStartMicros = -1; -int _buildFinishMicros = -1; -int _rasterStartMicros = -1; -int _rasterFinishMicros = -1; - -/// Records the vsync timestamp for this frame. -void frameTimingsOnVsync() { - if (!_frameTimingsEnabled) { - return; - } - _vsyncStartMicros = _nowMicros(); -} - -/// Records the time when the framework started building the frame. -void frameTimingsOnBuildStart() { - if (!_frameTimingsEnabled) { - return; - } - _buildStartMicros = _nowMicros(); -} - -/// Records the time when the framework finished building the frame. -void frameTimingsOnBuildFinish() { - if (!_frameTimingsEnabled) { - return; - } - _buildFinishMicros = _nowMicros(); -} - -/// Records the time when the framework started rasterizing the frame. -/// -/// On the web, this value is almost always the same as [_buildFinishMicros] -/// because it's single-threaded so there's no delay between building -/// and rasterization. -/// -/// This also means different things between HTML and CanvasKit renderers. -/// -/// In HTML "rasterization" only captures DOM updates, but not the work that -/// the browser performs after the DOM updates are committed. The browser -/// does not report that information. -/// -/// CanvasKit captures everything because we control the rasterization -/// process, so we know exactly when rasterization starts and ends. -void frameTimingsOnRasterStart() { - if (!_frameTimingsEnabled) { - return; - } - _rasterStartMicros = _nowMicros(); -} - -/// Records the time when the framework started rasterizing the frame. -/// -/// See [_frameTimingsOnRasterStart] for more details on what rasterization -/// timings mean on the web. -void frameTimingsOnRasterFinish() { - if (!_frameTimingsEnabled) { - return; - } - final int now = _nowMicros(); - _rasterFinishMicros = now; - _frameTimings.add(ui.FrameTiming( - vsyncStart: _vsyncStartMicros, - buildStart: _buildStartMicros, - buildFinish: _buildFinishMicros, - rasterStart: _rasterStartMicros, - rasterFinish: _rasterFinishMicros, - rasterFinishWallTime: _rasterFinishMicros, - )); - _vsyncStartMicros = -1; - _buildStartMicros = -1; - _buildFinishMicros = -1; - _rasterStartMicros = -1; - _rasterFinishMicros = -1; - if (now - _frameTimingsLastSubmitTime > _kFrameTimingsSubmitInterval) { - _frameTimingsLastSubmitTime = now; - EnginePlatformDispatcher.instance.invokeOnReportTimings(_frameTimings); - _frameTimings = []; - } -} - -/// Current timestamp in microseconds taken from the high-precision -/// monotonically increasing timer. -/// -/// See also: -/// -/// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now, -/// particularly notes about Firefox rounding to 1ms for security reasons, -/// which can be bypassed in tests by setting certain browser options. -int _nowMicros() { - return (domWindow.performance.now() * 1000).toInt(); -} - /// Counts various events that take place while the app is running. /// /// This class will slow down the app, and therefore should be disabled while diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index 23010c6db0bff..b137b70f4f909 100644 --- a/lib/web_ui/lib/src/engine/scene_view.dart +++ b/lib/web_ui/lib/src/engine/scene_view.dart @@ -9,20 +9,31 @@ import 'package:ui/ui.dart' as ui; const String kCanvasContainerTag = 'flt-canvas-container'; +typedef RenderResult = ({ + List imageBitmaps, + int rasterStartMicros, + int rasterEndMicros, +}); + // This is an interface that renders a `ScenePicture` as a `DomImageBitmap`. // It is optionally asynchronous. It is required for the `EngineSceneView` to // composite pictures into the canvases in the DOM tree it builds. abstract class PictureRenderer { - FutureOr renderPicture(ScenePicture picture); + FutureOr renderPictures(List picture); } class _SceneRender { - _SceneRender(this.scene, this._completer) { + _SceneRender( + this.scene, + this._completer, { + this.recorder, + }) { scene.beginRender(); } final EngineScene scene; final Completer _completer; + final FrameTimingRecorder? recorder; void done() { scene.endRender(); @@ -47,24 +58,24 @@ class EngineSceneView { _SceneRender? _currentRender; _SceneRender? _nextRender; - Future renderScene(EngineScene scene) { + Future renderScene(EngineScene scene, FrameTimingRecorder? recorder) { if (_currentRender != null) { // If a scene is already queued up, drop it and queue this one up instead // so that the scene view always displays the most recently requested scene. _nextRender?.done(); final Completer completer = Completer(); - _nextRender = _SceneRender(scene, completer); + _nextRender = _SceneRender(scene, completer, recorder: recorder); return completer.future; } final Completer completer = Completer(); - _currentRender = _SceneRender(scene, completer); + _currentRender = _SceneRender(scene, completer, recorder: recorder); _kickRenderLoop(); return completer.future; } Future _kickRenderLoop() async { final _SceneRender current = _currentRender!; - await _renderScene(current.scene); + await _renderScene(current.scene, current.recorder); current.done(); _currentRender = _nextRender; _nextRender = null; @@ -75,19 +86,33 @@ class EngineSceneView { } } - Future _renderScene(EngineScene scene) async { + Future _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async { final List slices = scene.rootLayer.slices; - final Iterable> renderFutures = slices.map( - (LayerSlice slice) async => switch (slice) { - PlatformViewSlice() => null, - PictureSlice() => pictureRenderer.renderPicture(slice.picture), - } - ); - final List renderedBitmaps = await Future.wait(renderFutures); + final List picturesToRender = []; + for (final LayerSlice slice in slices) { + if (slice is PictureSlice) { + picturesToRender.add(slice.picture); + } + } + final Map renderMap; + if (picturesToRender.isNotEmpty) { + final RenderResult renderResult = await pictureRenderer.renderPictures(picturesToRender); + renderMap = { + for (int i = 0; i < picturesToRender.length; i++) + picturesToRender[i]: renderResult.imageBitmaps[i], + }; + recorder?.recordRasterStart(renderResult.rasterStartMicros); + recorder?.recordRasterFinish(renderResult.rasterEndMicros); + } else { + renderMap = {}; + recorder?.recordRasterStart(); + recorder?.recordRasterFinish(); + } + recorder?.submitTimings(); + final List reusableContainers = List.from(containers); final List newContainers = []; - for (int i = 0; i < slices.length; i++) { - final LayerSlice slice = slices[i]; + for (final LayerSlice slice in slices) { switch (slice) { case PictureSlice(): PictureSliceContainer? container; @@ -106,7 +131,7 @@ class EngineSceneView { container = PictureSliceContainer(slice.picture.cullRect); } container.updateContents(); - container.renderBitmap(renderedBitmaps[i]!); + container.renderBitmap(renderMap[slice.picture]!); newContainers.add(container); case PlatformViewSlice(): diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart index ee32ffd987350..2b800ba276964 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart @@ -62,9 +62,9 @@ class SkwasmImage extends SkwasmObjectWrapper implements ui.Image { final ui.Canvas canvas = ui.Canvas(recorder); canvas.drawImage(this, ui.Offset.zero, ui.Paint()); final DomImageBitmap bitmap = - await (renderer as SkwasmRenderer).surface.renderPicture( - recorder.endRecording() as SkwasmPicture, - ); + (await (renderer as SkwasmRenderer).surface.renderPictures( + [recorder.endRecording() as SkwasmPicture], + )).imageBitmaps.single; final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(bitmap.width.toDartInt, bitmap.height.toDartInt); final DomCanvasRenderingContextBitmapRenderer context = @@ -75,8 +75,7 @@ class SkwasmImage extends SkwasmObjectWrapper implements ui.Image { // Zero out the contents of the canvas so that resources can be reclaimed // by the browser. - offscreenCanvas.width = 0; - offscreenCanvas.height = 0; + context.transferFromImageBitmap(null); return ByteData.view(arrayBuffer.toDart); } else { return (renderer as SkwasmRenderer).surface.rasterizeImage(this, format); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart index 3831188c4df63..22b7462eec9bc 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart @@ -35,10 +35,10 @@ external void surfaceSetCallbackHandler( isLeaf: true) external void surfaceDestroy(SurfaceHandle surface); -@Native( - symbol: 'surface_renderPicture', +@Native, Int)>( + symbol: 'surface_renderPictures', isLeaf: true) -external CallbackId surfaceRenderPicture(SurfaceHandle surface, PictureHandle picture); +external CallbackId surfaceRenderPictures(SurfaceHandle surface, Pointer picture, int count); @Native renderScene(ui.Scene scene, ui.FlutterView view) { + final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; + recorder?.recordBuildFinish(); + view as EngineFlutterView; assert(view is EngineFlutterWindow, 'Skwasm does not support multi-view mode yet'); final EngineSceneView sceneView = _getSceneViewForView(view); - return sceneView.renderScene(scene as EngineScene); + return sceneView.renderScene(scene as EngineScene, recorder); } EngineSceneView _getSceneViewForView(EngineFlutterView view) { @@ -477,6 +480,6 @@ class SkwasmPictureRenderer implements PictureRenderer { SkwasmSurface surface; @override - FutureOr renderPicture(ScenePicture picture) => - surface.renderPicture(picture as SkwasmPicture); + FutureOr renderPictures(List pictures) => + surface.renderPictures(pictures.cast()); } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart index 34af06d0d1fd7..eddcc19ff402b 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart @@ -12,6 +12,17 @@ import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; +@JS() +@staticInterop +@anonymous +class RasterResult {} + +extension RasterResultExtension on RasterResult { + external JSNumber get rasterStartMilliseconds; + external JSNumber get rasterEndMilliseconds; + external JSArray get imageBitmaps; +} + @pragma('wasm:export') WasmVoid callbackHandler(WasmI32 callbackId, WasmI32 context, WasmExternRef? jsContext) { // Actually hide this call behind whether skwasm is enabled. Otherwise, the SkwasmCallbackHandler @@ -78,11 +89,22 @@ class SkwasmSurface { surfaceSetCallbackHandler(handle, SkwasmCallbackHandler.instance.callbackPointer); } - Future renderPicture(SkwasmPicture picture) async { - final int callbackId = surfaceRenderPicture(handle, picture.handle); - final DomImageBitmap bitmap = (await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as DomImageBitmap; - return bitmap; - } + Future renderPictures(List pictures) => + withStackScope((StackScope scope) async { + final Pointer pictureHandles = + scope.allocPointerArray(pictures.length).cast(); + for (int i = 0; i < pictures.length; i++) { + pictureHandles[i] = pictures[i].handle; + } + final int callbackId = surfaceRenderPictures(handle, pictureHandles, pictures.length); + final RasterResult rasterResult = (await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as RasterResult; + final RenderResult result = ( + imageBitmaps: rasterResult.imageBitmaps.toDart.cast(), + rasterStartMicros: (rasterResult.rasterStartMilliseconds.toDartDouble * 1000).toInt(), + rasterEndMicros: (rasterResult.rasterEndMilliseconds.toDartDouble * 1000).toInt(), + ); + return result; + }); Future rasterizeImage(SkwasmImage image, ui.ImageByteFormat format) async { final int callbackId = surfaceRasterizeImage( diff --git a/lib/web_ui/skwasm/library_skwasm_support.js b/lib/web_ui/skwasm/library_skwasm_support.js index 5e62614e009c4..76cbac2db2a87 100644 --- a/lib/web_ui/skwasm/library_skwasm_support.js +++ b/lib/web_ui/skwasm/library_skwasm_support.js @@ -27,11 +27,18 @@ mergeInto(LibraryManager.library, { return; } switch (skwasmMessage) { - case 'renderPicture': - _surface_renderPictureOnWorker(data.surface, data.picture, data.callbackId); + case 'renderPictures': + _surface_renderPicturesOnWorker(data.surface, data.pictures, data.pictureCount, data.callbackId, performance.now()); return; case 'onRenderComplete': - _surface_onRenderComplete(data.surface, data.callbackId, data.imageBitmap); + _surface_onRenderComplete( + data.surface, + data.callbackId, { + "imageBitmaps": data.imageBitmaps, + "rasterStartMilliseconds": data.rasterStart, + "rasterEndMilliseconds": data.rasterEnd, + }, + ); return; case 'setAssociatedObject': associatedObjectsMap.set(data.pointer, data.object); @@ -54,11 +61,12 @@ mergeInto(LibraryManager.library, { PThread.pthreads[threadId].addEventListener("message", eventListener); } }; - _skwasm_dispatchRenderPicture = function(threadId, surfaceHandle, pictureHandle, callbackId) { + _skwasm_dispatchRenderPictures = function(threadId, surfaceHandle, pictures, pictureCount, callbackId) { PThread.pthreads[threadId].postMessage({ - skwasmMessage: 'renderPicture', + skwasmMessage: 'renderPictures', surface: surfaceHandle, - picture: pictureHandle, + pictures, + pictureCount, callbackId, }); }; @@ -85,15 +93,23 @@ mergeInto(LibraryManager.library, { canvas.width = width; canvas.height = height; }; - _skwasm_captureImageBitmap = async function(surfaceHandle, contextHandle, callbackId, width, height) { + _skwasm_captureImageBitmap = function(contextHandle, width, height, imagePromises) { + if (!imagePromises) imagePromises = Array(); const canvas = handleToCanvasMap.get(contextHandle); - const imageBitmap = await createImageBitmap(canvas, 0, 0, width, height); + imagePromises.push(createImageBitmap(canvas, 0, 0, width, height)); + return imagePromises; + }; + _skwasm_resolveAndPostImages = async function(surfaceHandle, imagePromises, rasterStart, callbackId) { + const imageBitmaps = imagePromises ? await Promise.all(imagePromises) : []; + const rasterEnd = performance.now(); postMessage({ skwasmMessage: 'onRenderComplete', surface: surfaceHandle, callbackId, - imageBitmap, - }, [imageBitmap]); + imageBitmaps, + rasterStart, + rasterEnd, + }, [...imageBitmaps]); }; _skwasm_createGlTextureFromTextureSource = function(textureSource, width, height) { const glCtx = GL.currentContext.GLctx; @@ -125,14 +141,16 @@ mergeInto(LibraryManager.library, { skwasm_disposeAssociatedObjectOnThread__deps: ['$skwasm_support_setup'], skwasm_registerMessageListener: function() {}, skwasm_registerMessageListener__deps: ['$skwasm_support_setup'], - skwasm_dispatchRenderPicture: function() {}, - skwasm_dispatchRenderPicture__deps: ['$skwasm_support_setup'], + skwasm_dispatchRenderPictures: function() {}, + skwasm_dispatchRenderPictures__deps: ['$skwasm_support_setup'], skwasm_createOffscreenCanvas: function () {}, skwasm_createOffscreenCanvas__deps: ['$skwasm_support_setup'], skwasm_resizeCanvas: function () {}, skwasm_resizeCanvas__deps: ['$skwasm_support_setup'], skwasm_captureImageBitmap: function () {}, skwasm_captureImageBitmap__deps: ['$skwasm_support_setup'], + skwasm_resolveAndPostImages: function () {}, + skwasm_resolveAndPostImages__deps: ['$skwasm_support_setup'], skwasm_createGlTextureFromTextureSource: function () {}, skwasm_createGlTextureFromTextureSource__deps: ['$skwasm_support_setup'], }); diff --git a/lib/web_ui/skwasm/skwasm_support.h b/lib/web_ui/skwasm/skwasm_support.h index ce36a192a69b6..c9132b89dd166 100644 --- a/lib/web_ui/skwasm/skwasm_support.h +++ b/lib/web_ui/skwasm/skwasm_support.h @@ -23,17 +23,21 @@ extern SkwasmObject skwasm_getAssociatedObject(void* pointer); extern void skwasm_disposeAssociatedObjectOnThread(unsigned long threadId, void* pointer); extern void skwasm_registerMessageListener(pthread_t threadId); -extern void skwasm_dispatchRenderPicture(unsigned long threadId, - Skwasm::Surface* surface, - SkPicture* picture, - uint32_t callbackId); +extern void skwasm_dispatchRenderPictures(unsigned long threadId, + Skwasm::Surface* surface, + sk_sp* pictures, + int count, + uint32_t callbackId); extern uint32_t skwasm_createOffscreenCanvas(int width, int height); extern void skwasm_resizeCanvas(uint32_t contextHandle, int width, int height); -extern void skwasm_captureImageBitmap(Skwasm::Surface* surfaceHandle, - uint32_t contextHandle, - uint32_t bitmapId, - int width, - int height); +extern SkwasmObject skwasm_captureImageBitmap(uint32_t contextHandle, + int width, + int height, + SkwasmObject imagePromises); +extern void skwasm_resolveAndPostImages(Skwasm::Surface* surface, + SkwasmObject imagePromises, + double rasterStart, + uint32_t callbackId); extern unsigned int skwasm_createGlTextureFromTextureSource( SkwasmObject textureSource, int width, diff --git a/lib/web_ui/skwasm/surface.cpp b/lib/web_ui/skwasm/surface.cpp index 28b64bd25a328..a1c24b4bad83e 100644 --- a/lib/web_ui/skwasm/surface.cpp +++ b/lib/web_ui/skwasm/surface.cpp @@ -39,11 +39,19 @@ void Surface::dispose() { } // Main thread only -uint32_t Surface::renderPicture(SkPicture* picture) { +uint32_t Surface::renderPictures(SkPicture** pictures, int count) { assert(emscripten_is_main_browser_thread()); uint32_t callbackId = ++_currentCallbackId; - picture->ref(); - skwasm_dispatchRenderPicture(_thread, this, picture, callbackId); + std::unique_ptr[]> picturePointers = + std::make_unique[]>(count); + for (int i = 0; i < count; i++) { + picturePointers[i] = sk_ref_sp(pictures[i]); + } + + // Releasing picturePointers here and will recreate the unique_ptr on the + // other thread See surface_renderPicturesOnWorker + skwasm_dispatchRenderPictures(_thread, this, picturePointers.release(), count, + callbackId); return callbackId; } @@ -136,20 +144,31 @@ void Surface::_recreateSurface() { } // Worker thread only -void Surface::renderPictureOnWorker(SkPicture* picture, uint32_t callbackId) { - SkRect pictureRect = picture->cullRect(); - SkIRect roundedOutRect; - pictureRect.roundOut(&roundedOutRect); - _resizeCanvasToFit(roundedOutRect.width(), roundedOutRect.height()); - SkMatrix matrix = - SkMatrix::Translate(-roundedOutRect.fLeft, -roundedOutRect.fTop); - makeCurrent(_glContext); - auto canvas = _surface->getCanvas(); - canvas->drawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc); - canvas->drawPicture(sk_ref_sp(picture), &matrix, nullptr); - _grContext->flush(_surface.get()); - skwasm_captureImageBitmap(this, _glContext, callbackId, - roundedOutRect.width(), roundedOutRect.height()); +void Surface::renderPicturesOnWorker(sk_sp* pictures, + int pictureCount, + uint32_t callbackId, + double rasterStart) { + // This is populated by the `captureImageBitmap` call the first time it is + // passed in. + SkwasmObject imagePromiseArray = __builtin_wasm_ref_null_extern(); + for (int i = 0; i < pictureCount; i++) { + sk_sp picture = pictures[i]; + SkRect pictureRect = picture->cullRect(); + SkIRect roundedOutRect; + pictureRect.roundOut(&roundedOutRect); + _resizeCanvasToFit(roundedOutRect.width(), roundedOutRect.height()); + SkMatrix matrix = + SkMatrix::Translate(-roundedOutRect.fLeft, -roundedOutRect.fTop); + makeCurrent(_glContext); + auto canvas = _surface->getCanvas(); + canvas->drawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc); + canvas->drawPicture(picture, &matrix, nullptr); + _grContext->flush(_surface.get()); + imagePromiseArray = + skwasm_captureImageBitmap(_glContext, roundedOutRect.width(), + roundedOutRect.height(), imagePromiseArray); + } + skwasm_resolveAndPostImages(this, imagePromiseArray, rasterStart, callbackId); } void Surface::_rasterizeImage(SkImage* image, @@ -225,16 +244,22 @@ SKWASM_EXPORT void surface_destroy(Surface* surface) { surface->dispose(); } -SKWASM_EXPORT uint32_t surface_renderPicture(Surface* surface, - SkPicture* picture) { - return surface->renderPicture(picture); +SKWASM_EXPORT uint32_t surface_renderPictures(Surface* surface, + SkPicture** pictures, + int count) { + return surface->renderPictures(pictures, count); } -SKWASM_EXPORT void surface_renderPictureOnWorker(Surface* surface, - SkPicture* picture, - uint32_t callbackId) { - surface->renderPictureOnWorker(picture, callbackId); - picture->unref(); +SKWASM_EXPORT void surface_renderPicturesOnWorker(Surface* surface, + sk_sp* pictures, + int pictureCount, + uint32_t callbackId, + double rasterStart) { + // This will release the pictures when they leave scope. + std::unique_ptr> picturesPointer = + std::unique_ptr>(pictures); + surface->renderPicturesOnWorker(pictures, pictureCount, callbackId, + rasterStart); } SKWASM_EXPORT uint32_t surface_rasterizeImage(Surface* surface, diff --git a/lib/web_ui/skwasm/surface.h b/lib/web_ui/skwasm/surface.h index 3577a947c782a..7e1d5fbb526e1 100644 --- a/lib/web_ui/skwasm/surface.h +++ b/lib/web_ui/skwasm/surface.h @@ -62,7 +62,7 @@ class Surface { // Main thread only void dispose(); - uint32_t renderPicture(SkPicture* picture); + uint32_t renderPictures(SkPicture** picture, int count); uint32_t rasterizeImage(SkImage* image, ImageByteFormat format); void setCallbackHandler(CallbackHandler* callbackHandler); void onRenderComplete(uint32_t callbackId, SkwasmObject imageBitmap); @@ -72,7 +72,10 @@ class Surface { SkwasmObject textureSource); // Worker thread - void renderPictureOnWorker(SkPicture* picture, uint32_t callbackId); + void renderPicturesOnWorker(sk_sp* picture, + int pictureCount, + uint32_t callbackId, + double rasterStart); private: void _runWorker(); diff --git a/lib/web_ui/test/canvaskit/frame_timings_test.dart b/lib/web_ui/test/canvaskit/frame_timings_test.dart deleted file mode 100644 index 0cacd4246616b..0000000000000 --- a/lib/web_ui/test/canvaskit/frame_timings_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; - -import '../common/frame_timings_common.dart'; -import 'common.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - group('frame timings', () { - setUpCanvasKitTest(withImplicitView: true); - - test('collects frame timings', () async { - await runFrameTimingsTest(); - }); - }); -} diff --git a/lib/web_ui/test/common/frame_timings_common.dart b/lib/web_ui/test/common/frame_timings_common.dart deleted file mode 100644 index 314e1a808861e..0000000000000 --- a/lib/web_ui/test/common/frame_timings_common.dart +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:test/test.dart'; -import 'package:ui/src/engine.dart' show EnginePlatformDispatcher; -import 'package:ui/ui.dart' as ui; - -/// Tests frame timings in a renderer-agnostic way. -/// -/// See CanvasKit-specific and HTML-specific test files `frame_timings_test.dart`. -Future runFrameTimingsTest() async { - final EnginePlatformDispatcher dispatcher = ui.PlatformDispatcher.instance as EnginePlatformDispatcher; - - List? timings; - dispatcher.onReportTimings = (List data) { - timings = data; - }; - Completer frameDone = Completer(); - dispatcher.onDrawFrame = () { - final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder - ..pushOffset(0, 0) - ..pop(); - dispatcher.render(sceneBuilder.build()).then((_) { - frameDone.complete(); - }); - }; - - // Frame 1. - dispatcher.scheduleFrame(); - await frameDone.future; - expect(timings, isNull, reason: "100 ms hasn't passed yet"); - await Future.delayed(const Duration(milliseconds: 150)); - - // Frame 2. - frameDone = Completer(); - dispatcher.scheduleFrame(); - await frameDone.future; - expect(timings, hasLength(2), reason: '100 ms passed. 2 frames pumped.'); - for (final ui.FrameTiming timing in timings!) { - expect(timing.vsyncOverhead, greaterThanOrEqualTo(Duration.zero)); - expect(timing.buildDuration, greaterThanOrEqualTo(Duration.zero)); - expect(timing.rasterDuration, greaterThanOrEqualTo(Duration.zero)); - expect(timing.totalSpan, greaterThanOrEqualTo(Duration.zero)); - expect(timing.layerCacheCount, equals(0)); - expect(timing.layerCacheBytes, equals(0)); - expect(timing.pictureCacheCount, equals(0)); - expect(timing.pictureCacheBytes, equals(0)); - } -} diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 48e84b717f5eb..93d54b09b226f 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -24,17 +24,23 @@ class StubPictureRenderer implements PictureRenderer { createDomCanvasElement(width: 500, height: 500); @override - Future renderPicture(ScenePicture picture) async { - renderedPictures.add(picture); - final ui.Rect cullRect = picture.cullRect; - final DomImageBitmap bitmap = - await createImageBitmap(scratchCanvasElement as JSObject, ( - x: 0, - y: 0, - width: cullRect.width.toInt(), - height: cullRect.height.toInt(), - )); - return bitmap; + Future renderPictures(List pictures) async { + renderedPictures.addAll(pictures); + final List bitmaps = await Future.wait(pictures.map((ScenePicture picture) { + final ui.Rect cullRect = picture.cullRect; + final Future bitmap = createImageBitmap(scratchCanvasElement as JSObject, ( + x: 0, + y: 0, + width: cullRect.width.toInt(), + height: cullRect.height.toInt(), + )); + return bitmap; + })); + return ( + imageBitmaps: bitmaps, + rasterStartMicros: 0, + rasterEndMicros: 0, + ); } List renderedPictures = []; @@ -65,7 +71,7 @@ void testMain() { final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PictureSlice(picture)); final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene); + await sceneView.renderScene(scene, null); final DomElement sceneElement = sceneView.sceneElement; final List children = sceneElement.children.toList(); @@ -100,7 +106,7 @@ void testMain() { final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PlatformViewSlice([platformView], null)); final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene); + await sceneView.renderScene(scene, null); final DomElement sceneElement = sceneView.sceneElement; final List children = sceneElement.children.toList(); @@ -134,7 +140,7 @@ void testMain() { final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PictureSlice(picture)); final EngineScene scene = EngineScene(rootLayer); - renderFutures.add(sceneView.renderScene(scene)); + renderFutures.add(sceneView.renderScene(scene, null)); } await Future.wait(renderFutures); diff --git a/lib/web_ui/test/engine/surface/frame_timings_test.dart b/lib/web_ui/test/engine/surface/frame_timings_test.dart deleted file mode 100644 index 14ec8f2e353da..0000000000000 --- a/lib/web_ui/test/engine/surface/frame_timings_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; - -import '../../common/frame_timings_common.dart'; -import '../../common/test_initialization.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - setUp(() async { - await bootstrapAndRunApp(withImplicitView: true); - }); - - test('collects frame timings', () async { - await runFrameTimingsTest(); - }); -} diff --git a/lib/web_ui/test/ui/frame_timings_test.dart b/lib/web_ui/test/ui/frame_timings_test.dart new file mode 100644 index 0000000000000..62f83b71d4d09 --- /dev/null +++ b/lib/web_ui/test/ui/frame_timings_test.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import '../common/test_initialization.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + setUp(() async { + await bootstrapAndRunApp(withImplicitView: true); + }); + + test('collects frame timings', () async { + final EnginePlatformDispatcher dispatcher = ui.PlatformDispatcher.instance as EnginePlatformDispatcher; + List? timings; + dispatcher.onReportTimings = (List data) { + timings = data; + }; + Completer frameDone = Completer(); + dispatcher.onDrawFrame = () { + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder + ..pushOffset(0, 0) + ..pop(); + dispatcher.render(sceneBuilder.build()).then((_) { + frameDone.complete(); + }); + }; + + // Frame 1. + dispatcher.scheduleFrame(); + await frameDone.future; + expect(timings, isNull, reason: "100 ms hasn't passed yet"); + await Future.delayed(const Duration(milliseconds: 150)); + + // Frame 2. + frameDone = Completer(); + dispatcher.scheduleFrame(); + await frameDone.future; + expect(timings, hasLength(2), reason: '100 ms passed. 2 frames pumped.'); + for (final ui.FrameTiming timing in timings!) { + expect(timing.vsyncOverhead, greaterThanOrEqualTo(Duration.zero)); + expect(timing.buildDuration, greaterThanOrEqualTo(Duration.zero)); + expect(timing.rasterDuration, greaterThanOrEqualTo(Duration.zero)); + expect(timing.totalSpan, greaterThanOrEqualTo(Duration.zero)); + expect(timing.layerCacheCount, equals(0)); + expect(timing.layerCacheBytes, equals(0)); + expect(timing.pictureCacheCount, equals(0)); + expect(timing.pictureCacheBytes, equals(0)); + } + }); +} diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index d8dc53049a5be..b7ca559ac7c64 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -22,18 +22,17 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; -import android.view.WindowMetrics; import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.annotation.VisibleForTesting; import io.flutter.Log; -import java.util.concurrent.Executor; -import java.util.function.Consumer; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; /* * A presentation used for hosting a single Android view in a virtual display. @@ -360,7 +359,7 @@ public Object getSystemService(String name) { private WindowManager getWindowManager() { if (windowManager == null) { - windowManager = windowManagerHandler; + windowManager = windowManagerHandler.getWindowManager(); } return windowManager; } @@ -378,18 +377,21 @@ private boolean isCalledFromAlertDialog() { } /* - * A static proxy handler for a WindowManager with custom overrides. + * A dynamic proxy handler for a WindowManager with custom overrides. * * The presentation's window manager delegates all calls to the default window manager. * WindowManager#addView calls triggered by views that are attached to the virtual display are crashing * (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded * WebView (as the selection handles are implemented as popup windows). * - * This static proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods - * to prevent these crashes, and forwards all other calls to the delegate. + * This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods + * to prevent these crashes. + * + * This will be more efficient as a static proxy that's not using reflection, but as the engine is currently + * not being built against the latest Android SDK we cannot override all relevant method. + * Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717 */ - @VisibleForTesting - static class WindowManagerHandler implements WindowManager { + static class WindowManagerHandler implements InvocationHandler { private static final String TAG = "PlatformViewsController"; private final WindowManager delegate; @@ -400,86 +402,72 @@ static class WindowManagerHandler implements WindowManager { fakeWindowRootView = fakeWindowViewGroup; } - @Override - @Deprecated - public Display getDefaultDisplay() { - return delegate.getDefaultDisplay(); + public WindowManager getWindowManager() { + return (WindowManager) + Proxy.newProxyInstance( + WindowManager.class.getClassLoader(), new Class[] {WindowManager.class}, this); } @Override - public void removeViewImmediate(View view) { - if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation"); - return; + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "addView": + addView(args); + return null; + case "removeView": + removeView(args); + return null; + case "removeViewImmediate": + removeViewImmediate(args); + return null; + case "updateViewLayout": + updateViewLayout(args); + return null; + } + try { + return method.invoke(delegate, args); + } catch (InvocationTargetException e) { + throw e.getCause(); } - view.clearAnimation(); - fakeWindowRootView.removeView(view); } - @Override - public void addView(View view, ViewGroup.LayoutParams params) { + private void addView(Object[] args) { if (fakeWindowRootView == null) { Log.w(TAG, "Embedded view called addView while detached from presentation"); return; } - fakeWindowRootView.addView(view, params); + View view = (View) args[0]; + WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1]; + fakeWindowRootView.addView(view, layoutParams); } - @Override - public void updateViewLayout(View view, ViewGroup.LayoutParams params) { + private void removeView(Object[] args) { if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation"); + Log.w(TAG, "Embedded view called removeView while detached from presentation"); return; } - fakeWindowRootView.updateViewLayout(view, params); + View view = (View) args[0]; + fakeWindowRootView.removeView(view); } - @Override - public void removeView(View view) { + private void removeViewImmediate(Object[] args) { if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called removeView while detached from presentation"); + Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation"); return; } + View view = (View) args[0]; + view.clearAnimation(); fakeWindowRootView.removeView(view); } - @RequiresApi(api = Build.VERSION_CODES.R) - @NonNull - @Override - public WindowMetrics getCurrentWindowMetrics() { - return delegate.getCurrentWindowMetrics(); - } - - @RequiresApi(api = Build.VERSION_CODES.R) - @NonNull - @Override - public WindowMetrics getMaximumWindowMetrics() { - return delegate.getMaximumWindowMetrics(); - } - - @RequiresApi(api = Build.VERSION_CODES.S) - @Override - public boolean isCrossWindowBlurEnabled() { - return delegate.isCrossWindowBlurEnabled(); - } - - @RequiresApi(api = Build.VERSION_CODES.S) - @Override - public void addCrossWindowBlurEnabledListener(@NonNull Consumer listener) { - delegate.addCrossWindowBlurEnabledListener(listener); - } - - @RequiresApi(api = Build.VERSION_CODES.S) - @Override - public void addCrossWindowBlurEnabledListener( - @NonNull Executor executor, @NonNull Consumer listener) { - delegate.addCrossWindowBlurEnabledListener(executor, listener); - } - - @RequiresApi(api = Build.VERSION_CODES.S) - @Override - public void removeCrossWindowBlurEnabledListener(@NonNull Consumer listener) { - delegate.removeCrossWindowBlurEnabledListener(listener); + private void updateViewLayout(Object[] args) { + if (fakeWindowRootView == null) { + Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation"); + return; + } + View view = (View) args[0]; + WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1]; + fakeWindowRootView.updateViewLayout(view, layoutParams); } } diff --git a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java index d99d344568d25..d27e08bbbdc97 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java @@ -7,26 +7,18 @@ import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.R; -import static android.os.Build.VERSION_CODES.S; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import android.annotation.TargetApi; import android.content.Context; import android.hardware.display.DisplayManager; import android.view.Display; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.util.concurrent.Executor; -import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; @@ -91,112 +83,4 @@ public void returnsOuterContextInputMethodManager_createDisplayContext() { // Android OS (or Robolectric's shadow, in this case). assertEquals(expected, actual); } - - @Test - @Config(minSdk = R) - public void windowManagerHandler_passesCorrectlyToFakeWindowViewGroup() { - // Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler. - WindowManager mockWindowManager = mock(WindowManager.class); - SingleViewPresentation.FakeWindowViewGroup mockFakeWindowViewGroup = - mock(SingleViewPresentation.FakeWindowViewGroup.class); - - View mockView = mock(View.class); - ViewGroup.LayoutParams mockLayoutParams = mock(ViewGroup.LayoutParams.class); - - SingleViewPresentation.WindowManagerHandler windowManagerHandler = - new SingleViewPresentation.WindowManagerHandler(mockWindowManager, mockFakeWindowViewGroup); - - // removeViewImmediate - windowManagerHandler.removeViewImmediate(mockView); - verify(mockView).clearAnimation(); - verify(mockFakeWindowViewGroup).removeView(mockView); - verifyNoInteractions(mockWindowManager); - - // addView - windowManagerHandler.addView(mockView, mockLayoutParams); - verify(mockFakeWindowViewGroup).addView(mockView, mockLayoutParams); - verifyNoInteractions(mockWindowManager); - - // updateViewLayout - windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); - verify(mockFakeWindowViewGroup).updateViewLayout(mockView, mockLayoutParams); - verifyNoInteractions(mockWindowManager); - - // removeView - windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); - verify(mockFakeWindowViewGroup).removeView(mockView); - verifyNoInteractions(mockWindowManager); - } - - @Test - @Config(minSdk = R) - public void windowManagerHandler_logAndReturnEarly_whenFakeWindowViewGroupIsNull() { - // Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler. - WindowManager mockWindowManager = mock(WindowManager.class); - - View mockView = mock(View.class); - ViewGroup.LayoutParams mockLayoutParams = mock(ViewGroup.LayoutParams.class); - - SingleViewPresentation.WindowManagerHandler windowManagerHandler = - new SingleViewPresentation.WindowManagerHandler(mockWindowManager, null); - - // removeViewImmediate - windowManagerHandler.removeViewImmediate(mockView); - verifyNoInteractions(mockView); - verifyNoInteractions(mockWindowManager); - - // addView - windowManagerHandler.addView(mockView, mockLayoutParams); - verifyNoInteractions(mockWindowManager); - - // updateViewLayout - windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); - verifyNoInteractions(mockWindowManager); - - // removeView - windowManagerHandler.updateViewLayout(mockView, mockLayoutParams); - verifyNoInteractions(mockWindowManager); - } - - // This section tests that WindowManagerHandler forwards all of the non-special case calls to the - // delegate WindowManager. Because this must include some deprecated WindowManager method calls - // (because the proxy overrides every method), we suppress deprecation warnings here. - @Test - @Config(minSdk = S) - @SuppressWarnings("deprecation") - public void windowManagerHandler_forwardsAllOtherCallsToDelegate() { - // Mock the WindowManager and FakeWindowViewGroup that get used by the WindowManagerHandler. - WindowManager mockWindowManager = mock(WindowManager.class); - SingleViewPresentation.FakeWindowViewGroup mockFakeWindowViewGroup = - mock(SingleViewPresentation.FakeWindowViewGroup.class); - - SingleViewPresentation.WindowManagerHandler windowManagerHandler = - new SingleViewPresentation.WindowManagerHandler(mockWindowManager, mockFakeWindowViewGroup); - - // Verify that all other calls get forwarded to the delegate. - Executor mockExecutor = mock(Executor.class); - @SuppressWarnings("Unchecked cast") - Consumer mockListener = (Consumer) mock(Consumer.class); - - windowManagerHandler.getDefaultDisplay(); - verify(mockWindowManager).getDefaultDisplay(); - - windowManagerHandler.getCurrentWindowMetrics(); - verify(mockWindowManager).getCurrentWindowMetrics(); - - windowManagerHandler.getMaximumWindowMetrics(); - verify(mockWindowManager).getMaximumWindowMetrics(); - - windowManagerHandler.isCrossWindowBlurEnabled(); - verify(mockWindowManager).isCrossWindowBlurEnabled(); - - windowManagerHandler.addCrossWindowBlurEnabledListener(mockListener); - verify(mockWindowManager).addCrossWindowBlurEnabledListener(mockListener); - - windowManagerHandler.addCrossWindowBlurEnabledListener(mockExecutor, mockListener); - verify(mockWindowManager).addCrossWindowBlurEnabledListener(mockExecutor, mockListener); - - windowManagerHandler.removeCrossWindowBlurEnabledListener(mockListener); - verify(mockWindowManager).removeCrossWindowBlurEnabledListener(mockListener); - } } diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java index a5415b5425199..ab874f45fb741 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java @@ -33,7 +33,13 @@ public void smokeTestEngineLaunch() throws Throwable { // Run the production under test on the UI thread instead of annotating the whole test // as @UiThreadTest because having the message handler and the CompletableFuture both being // on the same thread will create deadlocks. - UiThreadStatement.runOnUiThread(() -> engine.set(new FlutterEngine(applicationContext))); + UiThreadStatement.runOnUiThread( + () -> + engine.set( + new FlutterEngine( + applicationContext, + /*dartVmArgs */ null, + /* automaticallyRegisterPlugins */ false))); SettableFuture statusReceived = SettableFuture.create(); // Resolve locale to `en_US`. diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnMultiEngineActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnMultiEngineActivity.java index 02fe7a24127b2..d95adbd29c8df 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnMultiEngineActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnMultiEngineActivity.java @@ -16,14 +16,16 @@ public class SpawnMultiEngineActivity extends TestActivity { @NonNull public FlutterEngine provideFlutterEngine(@NonNull Context context) { FlutterEngineGroup engineGroup = new FlutterEngineGroup(context); - FlutterEngine firstEngine = engineGroup.createAndRunDefaultEngine(context); + FlutterEngineGroup.Options options = + new FlutterEngineGroup.Options(context).setAutomaticallyRegisterPlugins(false); + FlutterEngine firstEngine = engineGroup.createAndRunEngine(options); - FlutterEngine secondEngine = engineGroup.createAndRunDefaultEngine(context); + FlutterEngine secondEngine = engineGroup.createAndRunEngine(options); // Check that a new engine can be spawned from the group even if the group's // original engine has been destroyed. firstEngine.destroy(); - FlutterEngine thirdEngine = engineGroup.createAndRunDefaultEngine(context); + FlutterEngine thirdEngine = engineGroup.createAndRunEngine(options); return thirdEngine; } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnedEngineActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnedEngineActivity.java index fbce67dede353..6026893f99011 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnedEngineActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SpawnedEngineActivity.java @@ -16,9 +16,11 @@ public class SpawnedEngineActivity extends TestActivity { @NonNull public FlutterEngine provideFlutterEngine(@NonNull Context context) { FlutterEngineGroup engineGroup = new FlutterEngineGroup(context); - engineGroup.createAndRunDefaultEngine(context); + FlutterEngineGroup.Options options = + new FlutterEngineGroup.Options(context).setAutomaticallyRegisterPlugins(false); + engineGroup.createAndRunEngine(options); - FlutterEngine secondEngine = engineGroup.createAndRunDefaultEngine(context); + FlutterEngine secondEngine = engineGroup.createAndRunEngine(options); secondEngine .getDartExecutor() diff --git a/testing/scenario_app/run_ios_tests.sh b/testing/scenario_app/run_ios_tests.sh index 9b34497bb5094..ed040b16d1b47 100755 --- a/testing/scenario_app/run_ios_tests.sh +++ b/testing/scenario_app/run_ios_tests.sh @@ -58,13 +58,34 @@ zip_and_upload_xcresult_to_luci () { exit 1 } +readonly DEVICE_NAME="iPhone SE (3rd generation)" +readonly DEVICE=com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation +readonly OS_RUNTIME=com.apple.CoreSimulator.SimRuntime.iOS-17-0 +readonly OS="17.0" + +# Delete any existing devices named "iPhone SE (3rd generation)". Having more +# than one may cause issues when builds target the device. +echo "Deleting any existing devices names $DEVICE_NAME..." +RESULT=0 +while [[ $RESULT == 0 ]]; do + xcrun simctl delete "$DEVICE_NAME" || RESULT=1 + if [ $RESULT == 0 ]; then + echo "Deleted $DEVICE_NAME" + fi +done +echo "" + +echo "Creating $DEVICE_NAME $DEVICE $OS_RUNTIME ..." +xcrun simctl create "$DEVICE_NAME" "$DEVICE" "$OS_RUNTIME" +echo "" + echo "Running simulator tests with Skia" echo "" if set -o pipefail && xcodebuild -sdk iphonesimulator \ -scheme Scenarios \ -resultBundlePath "$RESULT_BUNDLE_PATH/ios_scenario.xcresult" \ - -destination 'platform=iOS Simulator,OS=17.0,name=iPhone SE (3rd generation)' \ + -destination "platform=iOS Simulator,OS=$OS,name=$DEVICE_NAME" \ clean test \ FLUTTER_ENGINE="$FLUTTER_ENGINE"; then echo "test success." @@ -82,7 +103,7 @@ echo "" if set -o pipefail && xcodebuild -sdk iphonesimulator \ -scheme Scenarios \ -resultBundlePath "$RESULT_BUNDLE_PATH/ios_scenario.xcresult" \ - -destination 'platform=iOS Simulator,OS=17.0,name=iPhone SE (3rd generation)' \ + -destination "platform=iOS Simulator,OS=$OS,name=$DEVICE_NAME" \ clean test \ FLUTTER_ENGINE="$FLUTTER_ENGINE" \ -skip-testing ScenariosUITests/MultiplePlatformViewsBackgroundForegroundTest/testPlatformView \ diff --git a/third_party/txt/BUILD.gn b/third_party/txt/BUILD.gn index fea46cd4e4116..a653adbbdfe5b 100644 --- a/third_party/txt/BUILD.gn +++ b/third_party/txt/BUILD.gn @@ -147,6 +147,7 @@ if (enable_unittests) { sources = [ "tests/font_collection_tests.cc", + "tests/paragraph_builder_skia_tests.cc", "tests/paragraph_unittests.cc", "tests/txt_run_all_unittests.cc", ] diff --git a/third_party/txt/src/skia/paragraph_builder_skia.cc b/third_party/txt/src/skia/paragraph_builder_skia.cc index 443c2b9ca2b08..8c6381a6b8a5b 100644 --- a/third_party/txt/src/skia/paragraph_builder_skia.cc +++ b/third_party/txt/src/skia/paragraph_builder_skia.cc @@ -119,6 +119,7 @@ skt::ParagraphStyle ParagraphBuilderSkia::TxtToSkia(const ParagraphStyle& txt) { strut_style.setFontSize(SkDoubleToScalar(txt.strut_font_size)); strut_style.setHeight(SkDoubleToScalar(txt.strut_height)); strut_style.setHeightOverride(txt.strut_has_height_override); + strut_style.setHalfLeading(txt.strut_half_leading); std::vector strut_fonts; std::transform(txt.strut_font_families.begin(), txt.strut_font_families.end(), diff --git a/third_party/txt/src/skia/paragraph_builder_skia.h b/third_party/txt/src/skia/paragraph_builder_skia.h index f14f7fe41f591..6269285899f58 100644 --- a/third_party/txt/src/skia/paragraph_builder_skia.h +++ b/third_party/txt/src/skia/paragraph_builder_skia.h @@ -45,6 +45,8 @@ class ParagraphBuilderSkia : public ParagraphBuilder { virtual std::unique_ptr Build() override; private: + friend class SkiaParagraphBuilderTests_ParagraphStrutStyle_Test; + skia::textlayout::ParagraphPainter::PaintID CreatePaintID( const flutter::DlPaint& dl_paint); skia::textlayout::ParagraphStyle TxtToSkia(const ParagraphStyle& txt); diff --git a/third_party/txt/tests/paragraph_builder_skia_tests.cc b/third_party/txt/tests/paragraph_builder_skia_tests.cc new file mode 100644 index 0000000000000..3131624be0a66 --- /dev/null +++ b/third_party/txt/tests/paragraph_builder_skia_tests.cc @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "gtest/gtest.h" + +#include + +#include "skia/paragraph_builder_skia.h" +#include "txt/paragraph_style.h" + +namespace txt { + +class SkiaParagraphBuilderTests : public ::testing::Test { + public: + SkiaParagraphBuilderTests() {} + + void SetUp() override {} +}; + +TEST_F(SkiaParagraphBuilderTests, ParagraphStrutStyle) { + ParagraphStyle style = ParagraphStyle(); + auto collection = std::make_shared(); + auto builder = ParagraphBuilderSkia(style, collection, false); + + auto strut_style = builder.TxtToSkia(style).getStrutStyle(); + ASSERT_FALSE(strut_style.getHalfLeading()); + + style.strut_half_leading = true; + strut_style = builder.TxtToSkia(style).getStrutStyle(); + ASSERT_TRUE(strut_style.getHalfLeading()); +} +} // namespace txt