| // 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. |
| |
| #define FML_USED_ON_EMBEDDER |
| |
| #include <string> |
| #include <vector> |
| |
| #import <Metal/Metal.h> |
| |
| #include "embedder.h" |
| #include "flutter/fml/synchronization/count_down_latch.h" |
| #include "flutter/shell/platform/embedder/tests/embedder_assertions.h" |
| #include "flutter/shell/platform/embedder/tests/embedder_config_builder.h" |
| #include "flutter/shell/platform/embedder/tests/embedder_test.h" |
| #include "flutter/shell/platform/embedder/tests/embedder_test_context_metal.h" |
| #include "flutter/shell/platform/embedder/tests/embedder_unittests_util.h" |
| #include "flutter/testing/assertions_skia.h" |
| #include "flutter/testing/testing.h" |
| |
| // CREATE_NATIVE_ENTRY is leaky by design |
| // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) |
| |
| namespace flutter { |
| namespace testing { |
| |
| using EmbedderTest = testing::EmbedderTest; |
| |
| TEST_F(EmbedderTest, CanRenderGradientWithMetal) { |
| auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); |
| |
| EmbedderConfigBuilder builder(context); |
| |
| builder.SetDartEntrypoint("render_gradient"); |
| builder.SetMetalRendererConfig(SkISize::Make(800, 600)); |
| |
| auto rendered_scene = context.GetNextSceneImage(); |
| |
| auto engine = builder.LaunchEngine(); |
| ASSERT_TRUE(engine.is_valid()); |
| |
| // Send a window metrics events so frames may be scheduled. |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| event.width = 800; |
| event.height = 600; |
| event.pixel_ratio = 1.0; |
| ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); |
| |
| // TODO (https://github.com/flutter/flutter/issues/73590): re-enable once |
| // we are able to figure out why this fails on the bots. |
| // ASSERT_TRUE(ImageMatchesFixture("gradient_metal.png", rendered_scene)); |
| } |
| |
| static sk_sp<SkSurface> GetSurfaceFromTexture(const sk_sp<GrDirectContext>& skia_context, |
| SkISize texture_size, |
| void* texture) { |
| GrMtlTextureInfo info; |
| info.fTexture.reset([(id<MTLTexture>)texture retain]); |
| GrBackendTexture backend_texture(texture_size.width(), texture_size.height(), GrMipmapped::kNo, |
| info); |
| |
| return SkSurface::MakeFromBackendTexture(skia_context.get(), backend_texture, |
| kTopLeft_GrSurfaceOrigin, 1, kBGRA_8888_SkColorType, |
| nullptr, nullptr); |
| } |
| |
| TEST_F(EmbedderTest, ExternalTextureMetal) { |
| EmbedderTestContextMetal& context = reinterpret_cast<EmbedderTestContextMetal&>( |
| GetEmbedderContext(EmbedderTestContextType::kMetalContext)); |
| |
| const auto texture_size = SkISize::Make(800, 600); |
| const int64_t texture_id = 1; |
| |
| TestMetalContext* metal_context = context.GetTestMetalContext(); |
| TestMetalContext::TextureInfo texture_info = metal_context->CreateMetalTexture(texture_size); |
| |
| sk_sp<SkSurface> surface = |
| GetSurfaceFromTexture(metal_context->GetSkiaContext(), texture_size, texture_info.texture); |
| auto canvas = surface->getCanvas(); |
| canvas->clear(SK_ColorRED); |
| metal_context->GetSkiaContext()->flushAndSubmit(); |
| |
| std::vector<FlutterMetalTextureHandle> textures{texture_info.texture}; |
| |
| context.SetExternalTextureCallback( |
| [&](int64_t id, size_t w, size_t h, FlutterMetalExternalTexture* output) { |
| EXPECT_TRUE(w == texture_size.width()); |
| EXPECT_TRUE(h == texture_size.height()); |
| EXPECT_TRUE(texture_id == id); |
| output->num_textures = 1; |
| output->height = h; |
| output->width = w; |
| output->pixel_format = FlutterMetalExternalTexturePixelFormat::kRGBA; |
| output->textures = textures.data(); |
| return true; |
| }); |
| |
| EmbedderConfigBuilder builder(context); |
| |
| builder.SetDartEntrypoint("render_texture"); |
| builder.SetMetalRendererConfig(texture_size); |
| |
| auto engine = builder.LaunchEngine(); |
| ASSERT_TRUE(engine.is_valid()); |
| |
| ASSERT_EQ(FlutterEngineRegisterExternalTexture(engine.get(), texture_id), kSuccess); |
| |
| auto rendered_scene = context.GetNextSceneImage(); |
| |
| // Send a window metrics events so frames may be scheduled. |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| event.width = texture_size.width(); |
| event.height = texture_size.height(); |
| event.pixel_ratio = 1.0; |
| ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); |
| |
| ASSERT_TRUE(ImageMatchesFixture("external_texture_metal.png", rendered_scene)); |
| } |
| |
| TEST_F(EmbedderTest, MetalCompositorMustBeAbleToRenderPlatformViews) { |
| auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); |
| |
| EmbedderConfigBuilder builder(context); |
| builder.SetMetalRendererConfig(SkISize::Make(800, 600)); |
| builder.SetCompositor(); |
| builder.SetDartEntrypoint("can_composite_platform_views"); |
| |
| builder.SetRenderTargetType(EmbedderTestBackingStoreProducer::RenderTargetType::kMetalTexture); |
| |
| fml::CountDownLatch latch(3); |
| context.GetCompositor().SetNextPresentCallback( |
| [&](const FlutterLayer** layers, size_t layers_count) { |
| ASSERT_EQ(layers_count, 3u); |
| |
| { |
| FlutterBackingStore backing_store = *layers[0]->backing_store; |
| backing_store.struct_size = sizeof(backing_store); |
| backing_store.type = kFlutterBackingStoreTypeMetal; |
| backing_store.did_update = true; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypeBackingStore; |
| layer.backing_store = &backing_store; |
| layer.size = FlutterSizeMake(800.0, 600.0); |
| layer.offset = FlutterPointMake(0, 0); |
| |
| ASSERT_EQ(*layers[0], layer); |
| } |
| |
| { |
| FlutterPlatformView platform_view = *layers[1]->platform_view; |
| platform_view.struct_size = sizeof(platform_view); |
| platform_view.identifier = 42; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypePlatformView; |
| layer.platform_view = &platform_view; |
| layer.size = FlutterSizeMake(123.0, 456.0); |
| layer.offset = FlutterPointMake(1.0, 2.0); |
| |
| ASSERT_EQ(*layers[1], layer); |
| } |
| |
| { |
| FlutterBackingStore backing_store = *layers[2]->backing_store; |
| backing_store.struct_size = sizeof(backing_store); |
| backing_store.type = kFlutterBackingStoreTypeMetal; |
| backing_store.did_update = true; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypeBackingStore; |
| layer.backing_store = &backing_store; |
| layer.size = FlutterSizeMake(800.0, 600.0); |
| layer.offset = FlutterPointMake(0.0, 0.0); |
| |
| ASSERT_EQ(*layers[2], layer); |
| } |
| |
| latch.CountDown(); |
| }); |
| |
| context.AddNativeCallback( |
| "SignalNativeTest", |
| CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.CountDown(); })); |
| |
| auto engine = builder.LaunchEngine(); |
| |
| // Send a window metrics events so frames may be scheduled. |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| event.width = 800; |
| event.height = 600; |
| event.pixel_ratio = 1.0; |
| ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); |
| ASSERT_TRUE(engine.is_valid()); |
| |
| latch.Wait(); |
| } |
| |
| TEST_F(EmbedderTest, CanRenderSceneWithoutCustomCompositorMetal) { |
| auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); |
| |
| EmbedderConfigBuilder builder(context); |
| |
| builder.SetDartEntrypoint("can_render_scene_without_custom_compositor"); |
| builder.SetMetalRendererConfig(SkISize::Make(800, 600)); |
| |
| auto rendered_scene = context.GetNextSceneImage(); |
| |
| auto engine = builder.LaunchEngine(); |
| ASSERT_TRUE(engine.is_valid()); |
| |
| // Send a window metrics events so frames may be scheduled. |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| event.width = 800; |
| event.height = 600; |
| event.pixel_ratio = 1.0; |
| ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); |
| |
| ASSERT_TRUE(ImageMatchesFixture("scene_without_custom_compositor.png", rendered_scene)); |
| } |
| |
| TEST_F(EmbedderTest, CompositorMustBeAbleToRenderKnownSceneMetal) { |
| auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); |
| |
| EmbedderConfigBuilder builder(context); |
| builder.SetMetalRendererConfig(SkISize::Make(800, 600)); |
| builder.SetCompositor(); |
| builder.SetDartEntrypoint("can_composite_platform_views_with_known_scene"); |
| |
| builder.SetRenderTargetType(EmbedderTestBackingStoreProducer::RenderTargetType::kMetalTexture); |
| |
| fml::CountDownLatch latch(5); |
| |
| auto scene_image = context.GetNextSceneImage(); |
| |
| context.GetCompositor().SetNextPresentCallback( |
| [&](const FlutterLayer** layers, size_t layers_count) { |
| ASSERT_EQ(layers_count, 5u); |
| |
| // Layer Root |
| { |
| FlutterBackingStore backing_store = *layers[0]->backing_store; |
| backing_store.type = kFlutterBackingStoreTypeMetal; |
| backing_store.did_update = true; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypeBackingStore; |
| layer.backing_store = &backing_store; |
| layer.size = FlutterSizeMake(800.0, 600.0); |
| layer.offset = FlutterPointMake(0.0, 0.0); |
| |
| ASSERT_EQ(*layers[0], layer); |
| } |
| |
| // Layer 1 |
| { |
| FlutterPlatformView platform_view = *layers[1]->platform_view; |
| platform_view.struct_size = sizeof(platform_view); |
| platform_view.identifier = 1; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypePlatformView; |
| layer.platform_view = &platform_view; |
| layer.size = FlutterSizeMake(50.0, 150.0); |
| layer.offset = FlutterPointMake(20.0, 20.0); |
| |
| ASSERT_EQ(*layers[1], layer); |
| } |
| |
| // Layer 2 |
| { |
| FlutterBackingStore backing_store = *layers[2]->backing_store; |
| backing_store.type = kFlutterBackingStoreTypeMetal; |
| backing_store.did_update = true; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypeBackingStore; |
| layer.backing_store = &backing_store; |
| layer.size = FlutterSizeMake(800.0, 600.0); |
| layer.offset = FlutterPointMake(0.0, 0.0); |
| |
| ASSERT_EQ(*layers[2], layer); |
| } |
| |
| // Layer 3 |
| { |
| FlutterPlatformView platform_view = *layers[3]->platform_view; |
| platform_view.struct_size = sizeof(platform_view); |
| platform_view.identifier = 2; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypePlatformView; |
| layer.platform_view = &platform_view; |
| layer.size = FlutterSizeMake(50.0, 150.0); |
| layer.offset = FlutterPointMake(40.0, 40.0); |
| |
| ASSERT_EQ(*layers[3], layer); |
| } |
| |
| // Layer 4 |
| { |
| FlutterBackingStore backing_store = *layers[4]->backing_store; |
| backing_store.type = kFlutterBackingStoreTypeMetal; |
| backing_store.did_update = true; |
| |
| FlutterLayer layer = {}; |
| layer.struct_size = sizeof(layer); |
| layer.type = kFlutterLayerContentTypeBackingStore; |
| layer.backing_store = &backing_store; |
| layer.size = FlutterSizeMake(800.0, 600.0); |
| layer.offset = FlutterPointMake(0.0, 0.0); |
| |
| ASSERT_EQ(*layers[4], layer); |
| } |
| |
| latch.CountDown(); |
| }); |
| |
| context.GetCompositor().SetPlatformViewRendererCallback( |
| [&](const FlutterLayer& layer, GrDirectContext* context) -> sk_sp<SkImage> { |
| auto surface = CreateRenderSurface(layer, context); |
| auto canvas = surface->getCanvas(); |
| FML_CHECK(canvas != nullptr); |
| |
| switch (layer.platform_view->identifier) { |
| case 1: { |
| SkPaint paint; |
| // See dart test for total order. |
| paint.setColor(SK_ColorGREEN); |
| paint.setAlpha(127); |
| const auto& rect = SkRect::MakeWH(layer.size.width, layer.size.height); |
| canvas->drawRect(rect, paint); |
| latch.CountDown(); |
| } break; |
| case 2: { |
| SkPaint paint; |
| // See dart test for total order. |
| paint.setColor(SK_ColorMAGENTA); |
| paint.setAlpha(127); |
| const auto& rect = SkRect::MakeWH(layer.size.width, layer.size.height); |
| canvas->drawRect(rect, paint); |
| latch.CountDown(); |
| } break; |
| default: |
| // Asked to render an unknown platform view. |
| FML_CHECK(false) << "Test was asked to composite an unknown platform view."; |
| } |
| |
| return surface->makeImageSnapshot(); |
| }); |
| |
| context.AddNativeCallback( |
| "SignalNativeTest", |
| CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.CountDown(); })); |
| |
| auto engine = builder.LaunchEngine(); |
| |
| // Send a window metrics events so frames may be scheduled. |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| // Flutter still thinks it is 800 x 600. Only the root surface is rotated. |
| event.width = 800; |
| event.height = 600; |
| event.pixel_ratio = 1.0; |
| ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); |
| ASSERT_TRUE(engine.is_valid()); |
| |
| latch.Wait(); |
| |
| ASSERT_TRUE(ImageMatchesFixture("compositor.png", scene_image)); |
| } |
| |
| TEST_F(EmbedderTest, CreateInvalidBackingstoreMetalTexture) { |
| auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); |
| EmbedderConfigBuilder builder(context); |
| builder.SetMetalRendererConfig(SkISize::Make(800, 600)); |
| builder.SetCompositor(); |
| builder.SetRenderTargetType(EmbedderTestBackingStoreProducer::RenderTargetType::kMetalTexture); |
| builder.SetDartEntrypoint("invalid_backingstore"); |
| |
| class TestCollectOnce { |
| public: |
| // Collect() should only be called once |
| void Collect() { |
| ASSERT_FALSE(collected_); |
| collected_ = true; |
| } |
| |
| private: |
| bool collected_ = false; |
| }; |
| fml::AutoResetWaitableEvent latch; |
| |
| builder.GetCompositor().create_backing_store_callback = |
| [](const FlutterBackingStoreConfig* config, // |
| FlutterBackingStore* backing_store_out, // |
| void* user_data // |
| ) { |
| backing_store_out->type = kFlutterBackingStoreTypeMetal; |
| // Deliberately set this to be invalid |
| backing_store_out->user_data = nullptr; |
| backing_store_out->metal.texture.texture = 0; |
| backing_store_out->metal.struct_size = sizeof(FlutterMetalBackingStore); |
| backing_store_out->metal.texture.user_data = new TestCollectOnce(); |
| backing_store_out->metal.texture.destruction_callback = [](void* user_data) { |
| reinterpret_cast<TestCollectOnce*>(user_data)->Collect(); |
| }; |
| return true; |
| }; |
| |
| context.AddNativeCallback( |
| "SignalNativeTest", |
| CREATE_NATIVE_ENTRY([&latch](Dart_NativeArguments args) { latch.Signal(); })); |
| |
| auto engine = builder.LaunchEngine(); |
| |
| // Send a window metrics events so frames may be scheduled. |
| FlutterWindowMetricsEvent event = {}; |
| event.struct_size = sizeof(event); |
| event.width = 800; |
| event.height = 600; |
| event.pixel_ratio = 1.0; |
| ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); |
| ASSERT_TRUE(engine.is_valid()); |
| latch.Wait(); |
| } |
| |
| TEST_F(EmbedderTest, ExternalTextureMetalRefreshedTooOften) { |
| EmbedderTestContextMetal& context = reinterpret_cast<EmbedderTestContextMetal&>( |
| GetEmbedderContext(EmbedderTestContextType::kMetalContext)); |
| |
| TestMetalContext* metal_context = context.GetTestMetalContext(); |
| auto metal_texture = metal_context->CreateMetalTexture(SkISize::Make(100, 100)); |
| |
| std::vector<FlutterMetalTextureHandle> textures{metal_texture.texture}; |
| |
| bool resolve_called = false; |
| |
| EmbedderExternalTextureMetal::ExternalTextureCallback callback([&](int64_t id, size_t, size_t) { |
| resolve_called = true; |
| auto res = std::make_unique<FlutterMetalExternalTexture>(); |
| res->struct_size = sizeof(FlutterMetalExternalTexture); |
| res->width = res->height = 100; |
| res->pixel_format = FlutterMetalExternalTexturePixelFormat::kRGBA; |
| res->textures = textures.data(); |
| res->num_textures = 1; |
| return res; |
| }); |
| EmbedderExternalTextureMetal texture(1, callback); |
| |
| auto surface = TestMetalSurface::Create(*metal_context, SkISize::Make(100, 100)); |
| auto skia_surface = surface->GetSurface(); |
| auto canvas = skia_surface->getCanvas(); |
| |
| Texture* texture_ = &texture; |
| Texture::PaintContext ctx{ |
| .canvas = canvas, |
| .gr_context = surface->GetGrContext().get(), |
| }; |
| texture_->Paint(ctx, SkRect::MakeXYWH(0, 0, 100, 100), false, |
| SkSamplingOptions(SkFilterMode::kLinear)); |
| |
| EXPECT_TRUE(resolve_called); |
| resolve_called = false; |
| |
| texture_->Paint(ctx, SkRect::MakeXYWH(0, 0, 100, 100), false, |
| SkSamplingOptions(SkFilterMode::kLinear)); |
| |
| EXPECT_FALSE(resolve_called); |
| |
| texture_->MarkNewFrameAvailable(); |
| |
| texture_->Paint(ctx, SkRect::MakeXYWH(0, 0, 100, 100), false, |
| SkSamplingOptions(SkFilterMode::kLinear)); |
| |
| EXPECT_TRUE(resolve_called); |
| } |
| |
| } // namespace testing |
| } // namespace flutter |
| |
| // NOLINTEND(clang-analyzer-core.StackAddressEscape) |