From d683f9a0c75bdfe7dc2f1444acc1a33dd97e89e1 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 2 Feb 2022 00:55:21 -0800 Subject: [PATCH] Vulkan support in the Embedder API (flutter/engine#29391) --- .../ci/licenses_golden/licenses_flutter | 4 + engine/src/flutter/shell/gpu/BUILD.gn | 5 +- .../shell/gpu/gpu_surface_metal_delegate.h | 4 +- .../flutter/shell/gpu/gpu_surface_vulkan.cc | 130 ++++++++++---- .../flutter/shell/gpu/gpu_surface_vulkan.h | 30 ++-- .../shell/gpu/gpu_surface_vulkan_delegate.h | 33 +++- .../flutter/shell/platform/embedder/BUILD.gn | 16 ++ .../shell/platform/embedder/embedder.cc | 167 +++++++++++++++++ .../shell/platform/embedder/embedder.h | 125 ++++++++++++- .../embedder/embedder_surface_vulkan.cc | 155 ++++++++++++++++ .../embedder/embedder_surface_vulkan.h | 89 +++++++++ .../embedder/fixtures/vk_dpr_noxform.png | Bin 0 -> 37208 bytes .../embedder/fixtures/vk_gradient.png | Bin 0 -> 33546 bytes .../embedder/platform_view_embedder.cc | 13 ++ .../embedder/platform_view_embedder.h | 14 ++ .../embedder/tests/embedder_assertions.h | 30 ++++ .../embedder/tests/embedder_config_builder.cc | 92 ++++++++++ .../embedder/tests/embedder_config_builder.h | 8 + .../platform/embedder/tests/embedder_test.cc | 7 + .../platform/embedder/tests/embedder_test.h | 5 + .../embedder_test_backingstore_producer.cc | 93 +++++++++- .../embedder_test_backingstore_producer.h | 18 ++ .../tests/embedder_test_compositor_vulkan.cc | 111 ++++++++++++ .../tests/embedder_test_compositor_vulkan.h | 32 ++++ .../embedder/tests/embedder_test_context.h | 7 + .../tests/embedder_test_context_vulkan.cc | 61 +++++++ .../tests/embedder_test_context_vulkan.h | 52 ++++++ .../embedder/tests/embedder_unittests_gl.cc | 107 +++++------ .../embedder/tests/embedder_unittests_util.cc | 56 ++++++ .../embedder/tests/embedder_unittests_util.h | 35 +++- engine/src/flutter/testing/BUILD.gn | 5 + .../flutter/testing/test_vulkan_context.cc | 170 ++++++++++++++++-- .../src/flutter/testing/test_vulkan_context.h | 29 ++- .../src/flutter/testing/test_vulkan_image.cc | 24 +++ .../src/flutter/testing/test_vulkan_image.h | 46 +++++ .../flutter/testing/test_vulkan_surface.cc | 110 ++++++++++++ .../src/flutter/testing/test_vulkan_surface.h | 41 +++++ engine/src/flutter/vulkan/vulkan_device.cc | 39 +++- engine/src/flutter/vulkan/vulkan_device.h | 12 +- .../src/flutter/vulkan/vulkan_proc_table.cc | 38 ++-- engine/src/flutter/vulkan/vulkan_proc_table.h | 13 +- engine/src/flutter/vulkan/vulkan_swapchain.cc | 2 +- engine/src/flutter/vulkan/vulkan_window.cc | 15 +- engine/src/flutter/vulkan/vulkan_window.h | 6 +- 44 files changed, 1876 insertions(+), 173 deletions(-) create mode 100644 engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.cc create mode 100644 engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.h create mode 100644 engine/src/flutter/shell/platform/embedder/fixtures/vk_dpr_noxform.png create mode 100644 engine/src/flutter/shell/platform/embedder/fixtures/vk_gradient.png create mode 100644 engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.cc create mode 100644 engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.h create mode 100644 engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.cc create mode 100644 engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h create mode 100644 engine/src/flutter/testing/test_vulkan_image.cc create mode 100644 engine/src/flutter/testing/test_vulkan_image.h create mode 100644 engine/src/flutter/testing/test_vulkan_surface.cc create mode 100644 engine/src/flutter/testing/test_vulkan_surface.h diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 451829e0861..21443efbc68 100755 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -1365,6 +1365,8 @@ FILE: ../../../flutter/shell/platform/embedder/embedder_surface_metal.h FILE: ../../../flutter/shell/platform/embedder/embedder_surface_metal.mm FILE: ../../../flutter/shell/platform/embedder/embedder_surface_software.cc FILE: ../../../flutter/shell/platform/embedder/embedder_surface_software.h +FILE: ../../../flutter/shell/platform/embedder/embedder_surface_vulkan.cc +FILE: ../../../flutter/shell/platform/embedder/embedder_surface_vulkan.h FILE: ../../../flutter/shell/platform/embedder/embedder_task_runner.cc FILE: ../../../flutter/shell/platform/embedder/embedder_task_runner.h FILE: ../../../flutter/shell/platform/embedder/embedder_thread_host.cc @@ -1387,6 +1389,8 @@ FILE: ../../../flutter/shell/platform/embedder/fixtures/scene_without_custom_com FILE: ../../../flutter/shell/platform/embedder/fixtures/snapshot_large_scene.png FILE: ../../../flutter/shell/platform/embedder/fixtures/verifyb143464703.png FILE: ../../../flutter/shell/platform/embedder/fixtures/verifyb143464703_soft_noxform.png +FILE: ../../../flutter/shell/platform/embedder/fixtures/vk_dpr_noxform.png +FILE: ../../../flutter/shell/platform/embedder/fixtures/vk_gradient.png FILE: ../../../flutter/shell/platform/embedder/platform_view_embedder.cc FILE: ../../../flutter/shell/platform/embedder/platform_view_embedder.h FILE: ../../../flutter/shell/platform/embedder/test_utils/key_codes.h diff --git a/engine/src/flutter/shell/gpu/BUILD.gn b/engine/src/flutter/shell/gpu/BUILD.gn index 9687b94fbb2..ec1d377d963 100644 --- a/engine/src/flutter/shell/gpu/BUILD.gn +++ b/engine/src/flutter/shell/gpu/BUILD.gn @@ -43,8 +43,9 @@ source_set("gpu_surface_vulkan") { "gpu_surface_vulkan_delegate.cc", "gpu_surface_vulkan_delegate.h", ] - - deps = gpu_common_deps + [ "//flutter/vulkan" ] + deps = [ "//flutter/shell/platform/embedder:embedder_headers" ] + deps += gpu_common_deps + public_deps = [ "//flutter/vulkan" ] } source_set("gpu_surface_metal") { diff --git a/engine/src/flutter/shell/gpu/gpu_surface_metal_delegate.h b/engine/src/flutter/shell/gpu/gpu_surface_metal_delegate.h index 56112042fbc..cb3900e392c 100644 --- a/engine/src/flutter/shell/gpu/gpu_surface_metal_delegate.h +++ b/engine/src/flutter/shell/gpu/gpu_surface_metal_delegate.h @@ -75,14 +75,14 @@ class GPUSurfaceMetalDelegate { //------------------------------------------------------------------------------ /// @brief Returns the handle to the MTLTexture to render to. This is only - /// called when the specefied render target type is `kMTLTexture`. + /// called when the specified render target type is `kMTLTexture`. /// virtual GPUMTLTextureInfo GetMTLTexture(const SkISize& frame_info) const = 0; //------------------------------------------------------------------------------ /// @brief Presents the texture with `texture_id` to the "screen". /// `texture_id` corresponds to a texture that has been obtained by an earlier - /// call to `GetMTLTexture`. This is only called when the specefied render + /// call to `GetMTLTexture`. This is only called when the specified render /// target type is `kMTLTexture`. /// /// @see |GPUSurfaceMetalDelegate::GetMTLTexture| diff --git a/engine/src/flutter/shell/gpu/gpu_surface_vulkan.cc b/engine/src/flutter/shell/gpu/gpu_surface_vulkan.cc index 570783bffa1..6040f459cc0 100644 --- a/engine/src/flutter/shell/gpu/gpu_surface_vulkan.cc +++ b/engine/src/flutter/shell/gpu/gpu_surface_vulkan.cc @@ -5,68 +5,77 @@ #include "flutter/shell/gpu/gpu_surface_vulkan.h" #include "flutter/fml/logging.h" +#include "fml/trace_event.h" +#include "include/core/SkSize.h" +#include "third_party/swiftshader/include/vulkan/vulkan_core.h" namespace flutter { -GPUSurfaceVulkan::GPUSurfaceVulkan( - GPUSurfaceVulkanDelegate* delegate, - std::unique_ptr native_surface, - bool render_to_surface) - : GPUSurfaceVulkan(/*context=*/nullptr, - delegate, - std::move(native_surface), - render_to_surface) {} - -GPUSurfaceVulkan::GPUSurfaceVulkan( - const sk_sp& context, - GPUSurfaceVulkanDelegate* delegate, - std::unique_ptr native_surface, - bool render_to_surface) - : window_(context, - delegate->vk(), - std::move(native_surface), - render_to_surface), +GPUSurfaceVulkan::GPUSurfaceVulkan(GPUSurfaceVulkanDelegate* delegate, + const sk_sp& skia_context, + bool render_to_surface) + : delegate_(delegate), + skia_context_(skia_context), render_to_surface_(render_to_surface), weak_factory_(this) {} GPUSurfaceVulkan::~GPUSurfaceVulkan() = default; bool GPUSurfaceVulkan::IsValid() { - return window_.IsValid(); + return skia_context_ != nullptr; } std::unique_ptr GPUSurfaceVulkan::AcquireFrame( - const SkISize& size) { - SurfaceFrame::FramebufferInfo framebuffer_info; - framebuffer_info.supports_readback = true; + const SkISize& frame_size) { + if (!IsValid()) { + FML_LOG(ERROR) << "Vulkan surface was invalid."; + return nullptr; + } + + if (frame_size.isEmpty()) { + FML_LOG(ERROR) << "Vulkan surface was asked for an empty frame."; + return nullptr; + } - // TODO(38466): Refactor GPU surface APIs take into account the fact that an - // external view embedder may want to render to the root surface. if (!render_to_surface_) { return std::make_unique( - nullptr, std::move(framebuffer_info), + nullptr, SurfaceFrame::FramebufferInfo(), [](const SurfaceFrame& surface_frame, SkCanvas* canvas) { return true; }); } - auto surface = window_.AcquireSurface(); - - if (surface == nullptr) { + FlutterVulkanImage image = delegate_->AcquireImage(frame_size); + if (!image.image) { + FML_LOG(ERROR) << "Invalid VkImage given by the embedder."; return nullptr; } - SurfaceFrame::SubmitCallback callback = - [weak_this = weak_factory_.GetWeakPtr()](const SurfaceFrame&, - SkCanvas* canvas) -> bool { - // Frames are only ever acquired on the raster thread. This is also the - // thread on which the weak pointer factory is collected (as this instance - // is owned by the rasterizer). So this use of weak pointers is safe. - if (canvas == nullptr || !weak_this) { + sk_sp surface = CreateSurfaceFromVulkanImage( + reinterpret_cast(image.image), + static_cast(image.format), frame_size); + if (!surface) { + FML_LOG(ERROR) << "Could not create the SkSurface from the Vulkan image."; + return nullptr; + } + + SurfaceFrame::SubmitCallback callback = [image = image, delegate = delegate_]( + const SurfaceFrame&, + SkCanvas* canvas) -> bool { + TRACE_EVENT0("flutter", "GPUSurfaceVulkan::PresentImage"); + if (canvas == nullptr) { + FML_DLOG(ERROR) << "Canvas not available."; return false; } - return weak_this->window_.SwapBuffers(); + + canvas->flush(); + + return delegate->PresentImage(reinterpret_cast(image.image), + static_cast(image.format)); }; + + SurfaceFrame::FramebufferInfo framebuffer_info{.supports_readback = true}; + return std::make_unique( std::move(surface), std::move(framebuffer_info), std::move(callback)); } @@ -80,7 +89,54 @@ SkMatrix GPUSurfaceVulkan::GetRootTransformation() const { } GrDirectContext* GPUSurfaceVulkan::GetContext() { - return window_.GetSkiaGrContext(); + return skia_context_.get(); +} + +sk_sp GPUSurfaceVulkan::CreateSurfaceFromVulkanImage( + const VkImage image, + const VkFormat format, + const SkISize& size) { + GrVkImageInfo image_info = { + .fImage = image, + .fImageTiling = VK_IMAGE_TILING_OPTIMAL, + .fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .fFormat = format, + .fImageUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, + .fSampleCount = 1, + .fLevelCount = 1, + }; + GrBackendTexture backend_texture(size.width(), // + size.height(), // + image_info // + ); + + SkSurfaceProps surface_properties(0, kUnknown_SkPixelGeometry); + + return SkSurface::MakeFromBackendTexture( + skia_context_.get(), // context + backend_texture, // back-end texture + kTopLeft_GrSurfaceOrigin, // surface origin + 1, // sample count + ColorTypeFromFormat(format), // color type + SkColorSpace::MakeSRGB(), // color space + &surface_properties // surface properties + ); +} + +SkColorType GPUSurfaceVulkan::ColorTypeFromFormat(const VkFormat format) { + switch (format) { + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SRGB: + return SkColorType::kRGBA_8888_SkColorType; + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_B8G8R8A8_SRGB: + return SkColorType::kBGRA_8888_SkColorType; + default: + return SkColorType::kUnknown_SkColorType; + } } } // namespace flutter diff --git a/engine/src/flutter/shell/gpu/gpu_surface_vulkan.h b/engine/src/flutter/shell/gpu/gpu_surface_vulkan.h index 50c420e3879..b281ac8c0f1 100644 --- a/engine/src/flutter/shell/gpu/gpu_surface_vulkan.h +++ b/engine/src/flutter/shell/gpu/gpu_surface_vulkan.h @@ -11,29 +11,25 @@ #include "flutter/fml/macros.h" #include "flutter/fml/memory/weak_ptr.h" #include "flutter/shell/gpu/gpu_surface_vulkan_delegate.h" +#include "flutter/vulkan/vulkan_backbuffer.h" #include "flutter/vulkan/vulkan_native_surface.h" #include "flutter/vulkan/vulkan_window.h" #include "include/core/SkRefCnt.h" namespace flutter { +//------------------------------------------------------------------------------ +/// @brief A GPU surface backed by VkImages provided by a +/// GPUSurfaceVulkanDelegate. +/// class GPUSurfaceVulkan : public Surface { public: - //------------------------------------------------------------------------------ - /// @brief Create a GPUSurfaceVulkan which implicitly creates its own - /// GrDirectContext for Skia. - /// - GPUSurfaceVulkan(GPUSurfaceVulkanDelegate* delegate, - std::unique_ptr native_surface, - bool render_to_surface); - //------------------------------------------------------------------------------ /// @brief Create a GPUSurfaceVulkan while letting it reuse an existing /// GrDirectContext. /// - GPUSurfaceVulkan(const sk_sp& context, - GPUSurfaceVulkanDelegate* delegate, - std::unique_ptr native_surface, + GPUSurfaceVulkan(GPUSurfaceVulkanDelegate* delegate, + const sk_sp& context, bool render_to_surface); ~GPUSurfaceVulkan() override; @@ -50,11 +46,19 @@ class GPUSurfaceVulkan : public Surface { // |Surface| GrDirectContext* GetContext() override; + static SkColorType ColorTypeFromFormat(const VkFormat format); + private: - vulkan::VulkanWindow window_; - const bool render_to_surface_; + GPUSurfaceVulkanDelegate* delegate_; + sk_sp skia_context_; + bool render_to_surface_; fml::WeakPtrFactory weak_factory_; + + sk_sp CreateSurfaceFromVulkanImage(const VkImage image, + const VkFormat format, + const SkISize& size); + FML_DISALLOW_COPY_AND_ASSIGN(GPUSurfaceVulkan); }; diff --git a/engine/src/flutter/shell/gpu/gpu_surface_vulkan_delegate.h b/engine/src/flutter/shell/gpu/gpu_surface_vulkan_delegate.h index 9b1a4959595..023fe926190 100644 --- a/engine/src/flutter/shell/gpu/gpu_surface_vulkan_delegate.h +++ b/engine/src/flutter/shell/gpu/gpu_surface_vulkan_delegate.h @@ -6,16 +6,43 @@ #define FLUTTER_SHELL_GPU_GPU_SURFACE_VULKAN_DELEGATE_H_ #include "flutter/fml/memory/ref_ptr.h" +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/vulkan/vulkan_device.h" +#include "flutter/vulkan/vulkan_image.h" #include "flutter/vulkan/vulkan_proc_table.h" +#include "third_party/skia/include/core/SkSize.h" namespace flutter { +//------------------------------------------------------------------------------ +/// @brief Interface implemented by all platform surfaces that can present +/// a Vulkan backing store to the "screen". The GPU surface +/// abstraction (which abstracts the client rendering API) uses this +/// delegation pattern to tell the platform surface (which abstracts +/// how backing stores fulfilled by the selected client rendering +/// API end up on the "screen" on a particular platform) when the +/// rasterizer needs to allocate and present the Vulkan backing +/// store. +/// +/// @see |EmbedderSurfaceVulkan|. +/// class GPUSurfaceVulkanDelegate { public: - ~GPUSurfaceVulkanDelegate(); + virtual ~GPUSurfaceVulkanDelegate(); - // Obtain a reference to the Vulkan implementation's proc table. - virtual fml::RefPtr vk() = 0; + /// @brief Obtain a reference to the Vulkan implementation's proc table. + /// + virtual const vulkan::VulkanProcTable& vk() = 0; + + /// @brief Called by the engine to fetch a VkImage for writing the next + /// frame. + /// + virtual FlutterVulkanImage AcquireImage(const SkISize& size) = 0; + + /// @brief Called by the engine once a frame has been rendered to the image + /// and it's ready to be bound for further reading/writing. + /// + virtual bool PresentImage(VkImage image, VkFormat format) = 0; }; } // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/BUILD.gn b/engine/src/flutter/shell/platform/embedder/BUILD.gn index 622432d2c11..1e6ac775d5e 100644 --- a/engine/src/flutter/shell/platform/embedder/BUILD.gn +++ b/engine/src/flutter/shell/platform/embedder/BUILD.gn @@ -109,6 +109,13 @@ template("embedder_source_set") { deps += [ "//flutter/shell/platform/darwin/graphics" ] } + if (embedder_enable_vulkan) { + sources += [ + "embedder_surface_vulkan.cc", + "embedder_surface_vulkan.h", + ] + } + public_deps = [ ":embedder_headers" ] public_configs += [ @@ -168,6 +175,8 @@ test_fixtures("fixtures") { "fixtures/dpr_noxform.png", "fixtures/dpr_xform.png", "fixtures/gradient.png", + "fixtures/vk_dpr_noxform.png", + "fixtures/vk_gradient.png", "fixtures/gradient_metal.png", "fixtures/external_texture_metal.png", "fixtures/gradient_xform.png", @@ -250,6 +259,13 @@ if (enable_unittests) { } if (test_enable_vulkan) { + sources += [ + "tests/embedder_test_compositor_vulkan.cc", + "tests/embedder_test_compositor_vulkan.h", + "tests/embedder_test_context_vulkan.cc", + "tests/embedder_test_context_vulkan.h", + ] + deps += [ "//flutter/testing:vulkan", "//flutter/vulkan", diff --git a/engine/src/flutter/shell/platform/embedder/embedder.cc b/engine/src/flutter/shell/platform/embedder/embedder.cc index 838c4c49659..0f826a57010 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder.cc @@ -164,6 +164,26 @@ static bool IsMetalRendererConfigValid(const FlutterRendererConfig* config) { return device && command_queue && present && get_texture; } +static bool IsVulkanRendererConfigValid(const FlutterRendererConfig* config) { + if (config->type != kVulkan) { + return false; + } + + const FlutterVulkanRendererConfig* vulkan_config = &config->vulkan; + + if (!SAFE_EXISTS(vulkan_config, instance) || + !SAFE_EXISTS(vulkan_config, physical_device) || + !SAFE_EXISTS(vulkan_config, device) || + !SAFE_EXISTS(vulkan_config, queue) || + !SAFE_EXISTS(vulkan_config, get_instance_proc_address_callback) || + !SAFE_EXISTS(vulkan_config, get_next_image_callback) || + !SAFE_EXISTS(vulkan_config, present_image_callback)) { + return false; + } + + return true; +} + static bool IsRendererValid(const FlutterRendererConfig* config) { if (config == nullptr) { return false; @@ -176,6 +196,8 @@ static bool IsRendererValid(const FlutterRendererConfig* config) { return IsSoftwareRendererConfigValid(config); case kMetal: return IsMetalRendererConfigValid(config); + case kVulkan: + return IsVulkanRendererConfigValid(config); default: return false; } @@ -388,6 +410,89 @@ InferMetalPlatformViewCreationCallback( #endif } +static flutter::Shell::CreateCallback +InferVulkanPlatformViewCreationCallback( + const FlutterRendererConfig* config, + void* user_data, + flutter::PlatformViewEmbedder::PlatformDispatchTable + platform_dispatch_table, + std::unique_ptr + external_view_embedder) { + if (config->type != kVulkan) { + return nullptr; + } + +#ifdef SHELL_ENABLE_VULKAN + std::function + vulkan_get_instance_proc_address = + [ptr = config->vulkan.get_instance_proc_address_callback, user_data]( + VkInstance instance, const char* proc_name) -> void* { + return ptr(user_data, instance, proc_name); + }; + + auto vulkan_get_next_image = + [ptr = config->vulkan.get_next_image_callback, + user_data](const SkISize& frame_size) -> FlutterVulkanImage { + FlutterFrameInfo frame_info = { + .struct_size = sizeof(FlutterFrameInfo), + .size = {static_cast(frame_size.width()), + static_cast(frame_size.height())}, + }; + + return ptr(user_data, &frame_info); + }; + + auto vulkan_present_image_callback = + [ptr = config->vulkan.present_image_callback, user_data]( + VkImage image, VkFormat format) -> bool { + FlutterVulkanImage image_desc = { + .struct_size = sizeof(FlutterVulkanImage), + .image = reinterpret_cast(image), + .format = static_cast(format), + }; + return ptr(user_data, &image_desc); + }; + + flutter::EmbedderSurfaceVulkan::VulkanDispatchTable vulkan_dispatch_table = { + .get_instance_proc_address = vulkan_get_instance_proc_address, + .get_next_image = vulkan_get_next_image, + .present_image = vulkan_present_image_callback, + }; + + std::shared_ptr view_embedder = + std::move(external_view_embedder); + + std::unique_ptr embedder_surface = + std::make_unique( + config->vulkan.version, + static_cast(config->vulkan.instance), + config->vulkan.enabled_instance_extension_count, + config->vulkan.enabled_instance_extensions, + config->vulkan.enabled_device_extension_count, + config->vulkan.enabled_device_extensions, + static_cast(config->vulkan.physical_device), + static_cast(config->vulkan.device), + config->vulkan.queue_family_index, + static_cast(config->vulkan.queue), vulkan_dispatch_table, + view_embedder); + + return fml::MakeCopyable( + [embedder_surface = std::move(embedder_surface), platform_dispatch_table, + external_view_embedder = + std::move(view_embedder)](flutter::Shell& shell) mutable { + return std::make_unique( + shell, // delegate + shell.GetTaskRunners(), // task runners + std::move(embedder_surface), // embedder surface + platform_dispatch_table, // platform dispatch table + std::move(external_view_embedder) // external view embedder + ); + }); +#else + return nullptr; +#endif +} + static flutter::Shell::CreateCallback InferSoftwarePlatformViewCreationCallback( const FlutterRendererConfig* config, @@ -450,6 +555,10 @@ InferPlatformViewCreationCallback( return InferMetalPlatformViewCreationCallback( config, user_data, platform_dispatch_table, std::move(external_view_embedder)); + case kVulkan: + return InferVulkanPlatformViewCreationCallback( + config, user_data, platform_dispatch_table, + std::move(external_view_embedder)); default: return nullptr; } @@ -628,6 +737,59 @@ static sk_sp MakeSkSurfaceFromBackingStore( #endif } +static sk_sp MakeSkSurfaceFromBackingStore( + GrDirectContext* context, + const FlutterBackingStoreConfig& config, + const FlutterVulkanBackingStore* vulkan) { +#ifdef SHELL_ENABLE_VULKAN + if (!vulkan->image) { + FML_LOG(ERROR) << "Embedder supplied null Vulkan image."; + return nullptr; + } + GrVkImageInfo image_info = { + .fImage = reinterpret_cast(vulkan->image->image), + .fImageTiling = VK_IMAGE_TILING_OPTIMAL, + .fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .fFormat = static_cast(vulkan->image->format), + .fImageUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, + .fSampleCount = 1, + .fLevelCount = 1, + }; + GrBackendTexture backend_texture(config.size.width, // + config.size.height, // + image_info // + ); + + SkSurfaceProps surface_properties(0, kUnknown_SkPixelGeometry); + + auto surface = SkSurface::MakeFromBackendTexture( + context, // context + backend_texture, // back-end texture + kTopLeft_GrSurfaceOrigin, // surface origin + 1, // sample count + flutter::GPUSurfaceVulkan::ColorTypeFromFormat( + static_cast(vulkan->image->format)), // color type + SkColorSpace::MakeSRGB(), // color space + &surface_properties, // surface properties + static_cast( + vulkan->destruction_callback), // release proc + vulkan->user_data // release context + ); + + if (!surface) { + FML_LOG(ERROR) << "Could not wrap embedder supplied Vulkan render texture."; + return nullptr; + } + + return surface; +#else + return nullptr; +#endif +} + static std::unique_ptr CreateEmbedderRenderTarget(const FlutterCompositor* compositor, const FlutterBackingStoreConfig& config, @@ -689,6 +851,11 @@ CreateEmbedderRenderTarget(const FlutterCompositor* compositor, render_surface = MakeSkSurfaceFromBackingStore(context, config, &backing_store.metal); break; + + case kFlutterBackingStoreTypeVulkan: + render_surface = + MakeSkSurfaceFromBackingStore(context, config, &backing_store.vulkan); + break; }; if (!render_surface) { diff --git a/engine/src/flutter/shell/platform/embedder/embedder.h b/engine/src/flutter/shell/platform/embedder/embedder.h index e8b769ef4cb..a8a2592c624 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.h +++ b/engine/src/flutter/shell/platform/embedder/embedder.h @@ -76,6 +76,7 @@ typedef enum { /// iOS version >= 10.0 (device), 13.0 (simulator) /// macOS version >= 10.14 kMetal, + kVulkan, } FlutterRendererType; /// Additional accessibility features that may be enabled by the platform. @@ -494,7 +495,7 @@ typedef struct { size_t struct_size; /// Embedder provided unique identifier to the texture buffer. Given that the /// `texture` handle is passed to the engine to render to, the texture buffer - /// is itseld owned by the embedder. This `texture_id` is then also given to + /// is itself owned by the embedder. This `texture_id` is then also given to /// the embedder in the present callback. int64_t texture_id; /// Handle to the MTLTexture that is owned by the embedder. Engine will render @@ -541,6 +542,104 @@ typedef struct { FlutterMetalTextureFrameCallback external_texture_frame_callback; } FlutterMetalRendererConfig; +/// Alias for VkInstance. +typedef void* FlutterVulkanInstanceHandle; + +/// Alias for VkPhysicalDevice. +typedef void* FlutterVulkanPhysicalDeviceHandle; + +/// Alias for VkDevice. +typedef void* FlutterVulkanDeviceHandle; + +/// Alias for VkQueue. +typedef void* FlutterVulkanQueueHandle; + +/// Alias for VkImage. +typedef uint64_t FlutterVulkanImageHandle; + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterVulkanImage). + size_t struct_size; + /// Handle to the VkImage that is owned by the embedder. The engine will + /// bind this image for writing the frame. + FlutterVulkanImageHandle image; + /// The VkFormat of the image (for example: VK_FORMAT_R8G8B8A8_UNORM). + uint32_t format; +} FlutterVulkanImage; + +/// Callback to fetch a Vulkan function pointer for a given instance. Normally, +/// this should return the results of vkGetInstanceProcAddr. +typedef void* (*FlutterVulkanInstanceProcAddressCallback)( + void* /* user data */, + FlutterVulkanInstanceHandle /* instance */, + const char* /* name */); + +/// Callback for when a VkImage is requested. +typedef FlutterVulkanImage (*FlutterVulkanImageCallback)( + void* /* user data */, + const FlutterFrameInfo* /* frame info */); + +/// Callback for when a VkImage has been written to and is ready for use by the +/// embedder. +typedef bool (*FlutterVulkanPresentCallback)( + void* /* user data */, + const FlutterVulkanImage* /* image */); + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterVulkanRendererConfig). + size_t struct_size; + + /// The Vulkan API version. This should match the value set in + /// VkApplicationInfo::apiVersion when the VkInstance was created. + uint32_t version; + /// VkInstance handle. Must not be destroyed before `FlutterEngineShutdown` is + /// called. + FlutterVulkanInstanceHandle instance; + /// VkPhysicalDevice handle. + FlutterVulkanPhysicalDeviceHandle physical_device; + /// VkDevice handle. Must not be destroyed before `FlutterEngineShutdown` is + /// called. + FlutterVulkanDeviceHandle device; + /// The queue family index of the VkQueue supplied in the next field. + uint32_t queue_family_index; + /// VkQueue handle. + FlutterVulkanQueueHandle queue; + /// The number of instance extensions available for enumerating in the next + /// field. + size_t enabled_instance_extension_count; + /// Array of enabled instance extension names. This should match the names + /// passed to `VkInstanceCreateInfo.ppEnabledExtensionNames` when the instance + /// was created, but any subset of enabled instance extensions may be + /// specified. + /// This field is optional; `nullptr` may be specified. + /// This memory is only accessed during the call to FlutterEngineInitialize. + const char** enabled_instance_extensions; + /// The number of device extensions available for enumerating in the next + /// field. + size_t enabled_device_extension_count; + /// Array of enabled logical device extension names. This should match the + /// names passed to `VkDeviceCreateInfo.ppEnabledExtensionNames` when the + /// logical device was created, but any subset of enabled logical device + /// extensions may be specified. + /// This field is optional; `nullptr` may be specified. + /// This memory is only accessed during the call to FlutterEngineInitialize. + /// For example: VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME + const char** enabled_device_extensions; + /// The callback invoked when resolving Vulkan function pointers. + FlutterVulkanInstanceProcAddressCallback get_instance_proc_address_callback; + /// The callback invoked when the engine requests a VkImage from the embedder + /// for rendering the next frame. + /// Not used if a FlutterCompositor is supplied in FlutterProjectArgs. + FlutterVulkanImageCallback get_next_image_callback; + /// The callback invoked when a VkImage has been written to and is ready for + /// use by the embedder. Prior to calling this callback, the engine performs + /// a host sync, and so the VkImage can be used in a pipeline by the embedder + /// without any additional synchronization. + /// Not used if a FlutterCompositor is supplied in FlutterProjectArgs. + FlutterVulkanPresentCallback present_image_callback; + +} FlutterVulkanRendererConfig; + typedef struct { /// The size of this struct. Must be sizeof(FlutterSoftwareRendererConfig). size_t struct_size; @@ -557,6 +656,7 @@ typedef struct { FlutterOpenGLRendererConfig open_gl; FlutterSoftwareRendererConfig software; FlutterMetalRendererConfig metal; + FlutterVulkanRendererConfig vulkan; }; } FlutterRendererConfig; @@ -989,6 +1089,25 @@ typedef struct { }; } FlutterMetalBackingStore; +typedef struct { + /// The size of this struct. Must be sizeof(FlutterVulkanBackingStore). + size_t struct_size; + /// The image that the layer will be rendered to. This image must already be + /// available for the engine to bind for writing when it's given to the engine + /// via the backing store creation callback. The engine will perform a host + /// sync for all layers prior to calling the compositor present callback, and + /// so the written layer images can be freely bound by the embedder without + /// any additional synchronization. + const FlutterVulkanImage* image; + /// A baton that is not interpreted by the engine in any way. It will be given + /// back to the embedder in the destruction callback below. Embedder resources + /// may be associated with this baton. + void* user_data; + /// The callback invoked by the engine when it no longer needs this backing + /// store. + VoidCallback destruction_callback; +} FlutterVulkanBackingStore; + typedef enum { /// Indicates that the Flutter application requested that an opacity be /// applied to the platform view. @@ -1048,6 +1167,8 @@ typedef enum { kFlutterBackingStoreTypeSoftware, /// Specifies a Metal backing store. This is backed by a Metal texture. kFlutterBackingStoreTypeMetal, + /// Specifies a Vulkan backing store. This is backed by a Vulkan VkImage. + kFlutterBackingStoreTypeVulkan, } FlutterBackingStoreType; typedef struct { @@ -1069,6 +1190,8 @@ typedef struct { FlutterSoftwareBackingStore software; // The description of the Metal backing store. FlutterMetalBackingStore metal; + // The description of the Vulkan backing store. + FlutterVulkanBackingStore vulkan; }; } FlutterBackingStore; diff --git a/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.cc b/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.cc new file mode 100644 index 00000000000..b3918a24200 --- /dev/null +++ b/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.cc @@ -0,0 +1,155 @@ +// 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. + +#include "flutter/shell/platform/embedder/embedder_surface_vulkan.h" + +#include "flutter/shell/common/shell_io_manager.h" +#include "include/gpu/GrDirectContext.h" +#include "include/gpu/vk/GrVkBackendContext.h" +#include "include/gpu/vk/GrVkExtensions.h" +#include "shell/gpu/gpu_surface_vulkan.h" +#include "shell/gpu/gpu_surface_vulkan_delegate.h" + +namespace flutter { + +EmbedderSurfaceVulkan::EmbedderSurfaceVulkan( + uint32_t version, + VkInstance instance, + size_t instance_extension_count, + const char** instance_extensions, + size_t device_extension_count, + const char** device_extensions, + VkPhysicalDevice physical_device, + VkDevice device, + uint32_t queue_family_index, + VkQueue queue, + VulkanDispatchTable vulkan_dispatch_table, + std::shared_ptr external_view_embedder) + : vk_(fml::MakeRefCounted( + vulkan_dispatch_table.get_instance_proc_address)), + device_(*vk_, + vulkan::VulkanHandle{physical_device}, + vulkan::VulkanHandle{device}, + queue_family_index, + vulkan::VulkanHandle{queue}), + vulkan_dispatch_table_(vulkan_dispatch_table), + external_view_embedder_(external_view_embedder) { + // Make sure all required members of the dispatch table are checked. + if (!vulkan_dispatch_table_.get_instance_proc_address || + !vulkan_dispatch_table_.get_next_image || + !vulkan_dispatch_table_.present_image) { + return; + } + + vk_->SetupInstanceProcAddresses(vulkan::VulkanHandle{instance}); + vk_->SetupDeviceProcAddresses(vulkan::VulkanHandle{device}); + if (!vk_->IsValid()) { + FML_LOG(ERROR) << "VulkanProcTable invalid."; + return; + } + + main_context_ = CreateGrContext(instance, version, instance_extension_count, + instance_extensions, device_extension_count, + device_extensions, ContextType::kRender); + // TODO(96954): Add a second (optional) queue+family index to the Embedder API + // to allow embedders to specify a dedicated transfer queue for + // use by the resource context. Queue families with graphics + // capability can always be used for memory transferring, but it + // would be advantageous to use a dedicated transter queue here. + resource_context_ = CreateGrContext( + instance, version, instance_extension_count, instance_extensions, + device_extension_count, device_extensions, ContextType::kResource); + + valid_ = main_context_ && resource_context_; +} + +EmbedderSurfaceVulkan::~EmbedderSurfaceVulkan() { + if (main_context_) { + main_context_->releaseResourcesAndAbandonContext(); + } + if (resource_context_) { + resource_context_->releaseResourcesAndAbandonContext(); + } +} + +// |GPUSurfaceVulkanDelegate| +const vulkan::VulkanProcTable& EmbedderSurfaceVulkan::vk() { + return *vk_; +} + +// |GPUSurfaceVulkanDelegate| +FlutterVulkanImage EmbedderSurfaceVulkan::AcquireImage(const SkISize& size) { + return vulkan_dispatch_table_.get_next_image(size); +} + +// |GPUSurfaceVulkanDelegate| +bool EmbedderSurfaceVulkan::PresentImage(VkImage image, VkFormat format) { + return vulkan_dispatch_table_.present_image(image, format); +} + +// |EmbedderSurface| +bool EmbedderSurfaceVulkan::IsValid() const { + return valid_; +} + +// |EmbedderSurface| +std::unique_ptr EmbedderSurfaceVulkan::CreateGPUSurface() { + const bool render_to_surface = !external_view_embedder_; + return std::make_unique(this, main_context_, + render_to_surface); +} + +// |EmbedderSurface| +sk_sp EmbedderSurfaceVulkan::CreateResourceContext() const { + return resource_context_; +} + +sk_sp EmbedderSurfaceVulkan::CreateGrContext( + VkInstance instance, + uint32_t version, + size_t instance_extension_count, + const char** instance_extensions, + size_t device_extension_count, + const char** device_extensions, + ContextType context_type) const { + uint32_t skia_features = 0; + if (!device_.GetPhysicalDeviceFeaturesSkia(&skia_features)) { + FML_LOG(ERROR) << "Failed to get physical device features."; + + return nullptr; + } + + auto get_proc = vk_->CreateSkiaGetProc(); + if (get_proc == nullptr) { + FML_LOG(ERROR) << "Failed to create Vulkan getProc for Skia."; + return nullptr; + } + + GrVkExtensions extensions; + + GrVkBackendContext backend_context = {}; + backend_context.fInstance = instance; + backend_context.fPhysicalDevice = device_.GetPhysicalDeviceHandle(); + backend_context.fDevice = device_.GetHandle(); + backend_context.fQueue = device_.GetQueueHandle(); + backend_context.fGraphicsQueueIndex = device_.GetGraphicsQueueIndex(); + backend_context.fMinAPIVersion = version; + backend_context.fMaxAPIVersion = version; + backend_context.fFeatures = skia_features; + backend_context.fVkExtensions = &extensions; + backend_context.fGetProc = get_proc; + backend_context.fOwnsInstanceAndDevice = false; + + extensions.init(backend_context.fGetProc, backend_context.fInstance, + backend_context.fPhysicalDevice, instance_extension_count, + instance_extensions, device_extension_count, + device_extensions); + + GrContextOptions options = + MakeDefaultContextOptions(context_type, GrBackendApi::kVulkan); + options.fReduceOpsTaskSplitting = GrContextOptions::Enable::kNo; + return GrDirectContext::MakeVulkan(backend_context, options); +} + +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.h b/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.h new file mode 100644 index 00000000000..3fa28fa09a7 --- /dev/null +++ b/engine/src/flutter/shell/platform/embedder/embedder_surface_vulkan.h @@ -0,0 +1,89 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_SURFACE_VULKAN_H_ +#define FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_SURFACE_VULKAN_H_ + +#include "flutter/fml/macros.h" +#include "flutter/shell/gpu/gpu_surface_vulkan.h" +#include "flutter/shell/platform/embedder/embedder_external_view_embedder.h" +#include "flutter/shell/platform/embedder/embedder_surface.h" +#include "shell/common/context_options.h" +#include "shell/gpu/gpu_surface_vulkan_delegate.h" +#include "shell/platform/embedder/embedder.h" +#include "vulkan/vulkan_proc_table.h" + +namespace flutter { + +class EmbedderSurfaceVulkan final : public EmbedderSurface, + public GPUSurfaceVulkanDelegate { + public: + struct VulkanDispatchTable { + std::function + get_instance_proc_address; // required + std::function + get_next_image; // required + std::function + present_image; // required + }; + + EmbedderSurfaceVulkan( + uint32_t version, + VkInstance instance, + size_t instance_extension_count, + const char** instance_extensions, + size_t device_extension_count, + const char** device_extensions, + VkPhysicalDevice physical_device, + VkDevice device, + uint32_t queue_family_index, + VkQueue queue, + VulkanDispatchTable vulkan_dispatch_table, + std::shared_ptr external_view_embedder); + + ~EmbedderSurfaceVulkan() override; + + // |GPUSurfaceVulkanDelegate| + const vulkan::VulkanProcTable& vk() override; + + // |GPUSurfaceVulkanDelegate| + FlutterVulkanImage AcquireImage(const SkISize& size) override; + + // |GPUSurfaceVulkanDelegate| + bool PresentImage(VkImage image, VkFormat format) override; + + private: + bool valid_ = false; + fml::RefPtr vk_; + vulkan::VulkanDevice device_; + VulkanDispatchTable vulkan_dispatch_table_; + std::shared_ptr external_view_embedder_; + sk_sp main_context_; + sk_sp resource_context_; + + // |EmbedderSurface| + bool IsValid() const override; + + // |EmbedderSurface| + std::unique_ptr CreateGPUSurface() override; + + // |EmbedderSurface| + sk_sp CreateResourceContext() const override; + + sk_sp CreateGrContext(VkInstance instance, + uint32_t version, + size_t instance_extension_count, + const char** instance_extensions, + size_t device_extension_count, + const char** device_extensions, + ContextType context_type) const; + + void* GetInstanceProcAddress(VkInstance instance, const char* proc_name); + + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderSurfaceVulkan); +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_SURFACE_VULKAN_H_ diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/vk_dpr_noxform.png b/engine/src/flutter/shell/platform/embedder/fixtures/vk_dpr_noxform.png new file mode 100644 index 0000000000000000000000000000000000000000..7f5c28ba808fd59714b7cfe1113478d25ce02863 GIT binary patch literal 37208 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 literal 0 HcmV?d00001 diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/vk_gradient.png b/engine/src/flutter/shell/platform/embedder/fixtures/vk_gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..b00a8baf8d2e5301bd978fc12d212e15ab02ed6c GIT binary patch literal 33546 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 A#{d8T literal 0 HcmV?d00001 diff --git a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc index 2a2752446f5..a24e13e2636 100644 --- a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc @@ -49,6 +49,19 @@ PlatformViewEmbedder::PlatformViewEmbedder( platform_dispatch_table_(platform_dispatch_table) {} #endif +#ifdef SHELL_ENABLE_VULKAN +PlatformViewEmbedder::PlatformViewEmbedder( + PlatformView::Delegate& delegate, + flutter::TaskRunners task_runners, + std::unique_ptr embedder_surface, + PlatformDispatchTable platform_dispatch_table, + std::shared_ptr external_view_embedder) + : PlatformView(delegate, std::move(task_runners)), + external_view_embedder_(external_view_embedder), + embedder_surface_(std::move(embedder_surface)), + platform_dispatch_table_(platform_dispatch_table) {} +#endif + PlatformViewEmbedder::~PlatformViewEmbedder() = default; void PlatformViewEmbedder::UpdateSemantics( diff --git a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h index 22f4d2dee6e..5c40ce1e16e 100644 --- a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h +++ b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h @@ -23,6 +23,10 @@ #include "flutter/shell/platform/embedder/embedder_surface_metal.h" #endif +#ifdef SHELL_ENABLE_VULKAN +#include "flutter/shell/platform/embedder/embedder_surface_vulkan.h" +#endif + namespace flutter { class PlatformViewEmbedder final : public PlatformView { @@ -79,6 +83,16 @@ class PlatformViewEmbedder final : public PlatformView { std::shared_ptr external_view_embedder); #endif +#ifdef SHELL_ENABLE_VULKAN + // Creates a platform view that sets up an Vulkan rasterizer. + PlatformViewEmbedder( + PlatformView::Delegate& delegate, + flutter::TaskRunners task_runners, + std::unique_ptr embedder_surface, + PlatformDispatchTable platform_dispatch_table, + std::shared_ptr external_view_embedder); +#endif + ~PlatformViewEmbedder() override; // |PlatformView| diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_assertions.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_assertions.h index 423c1239f34..e631305852a 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_assertions.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_assertions.h @@ -70,6 +70,16 @@ inline bool operator==(const FlutterMetalTexture& a, return a.texture_id == b.texture_id && a.texture == b.texture; } +inline bool operator==(const FlutterVulkanImage& a, + const FlutterVulkanImage& b) { + return a.image == b.image && a.format == b.format; +} + +inline bool operator==(const FlutterVulkanBackingStore& a, + const FlutterVulkanBackingStore& b) { + return a.image == b.image; +} + inline bool operator==(const FlutterMetalBackingStore& a, const FlutterMetalBackingStore& b) { return a.texture == b.texture; @@ -112,6 +122,8 @@ inline bool operator==(const FlutterBackingStore& a, return a.software == b.software; case kFlutterBackingStoreTypeMetal: return a.metal == b.metal; + case kFlutterBackingStoreTypeVulkan: + return a.vulkan == b.vulkan; } return false; @@ -230,6 +242,8 @@ inline std::string FlutterBackingStoreTypeToString( return "kFlutterBackingStoreTypeSoftware"; case kFlutterBackingStoreTypeMetal: return "kFlutterBackingStoreTypeMetal"; + case kFlutterBackingStoreTypeVulkan: + return "kFlutterBackingStoreTypeVulkan"; } return "Unknown"; } @@ -256,6 +270,13 @@ inline std::ostream& operator<<(std::ostream& out, << item.texture_id << std::dec << " Handle: 0x" << std::hex << item.texture; } + +inline std::ostream& operator<<(std::ostream& out, + const FlutterVulkanImage& item) { + return out << "(FlutterVulkanTexture) Image Handle: " << std::hex + << item.image << std::dec << " Format: " << item.format; +} + inline std::string FlutterPlatformViewMutationTypeToString( FlutterPlatformViewMutationType type) { switch (type) { @@ -347,6 +368,11 @@ inline std::ostream& operator<<(std::ostream& out, return out << "(FlutterMetalBackingStore) Texture: " << item.texture; } +inline std::ostream& operator<<(std::ostream& out, + const FlutterVulkanBackingStore& item) { + return out << "(FlutterVulkanBackingStore) Image: " << item.image; +} + inline std::ostream& operator<<(std::ostream& out, const FlutterBackingStore& backing_store) { out << "(FlutterBackingStore) Struct size: " << backing_store.struct_size @@ -366,6 +392,10 @@ inline std::ostream& operator<<(std::ostream& out, case kFlutterBackingStoreTypeMetal: out << backing_store.metal; break; + + case kFlutterBackingStoreTypeVulkan: + out << backing_store.vulkan; + break; } return out; diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc index d04188e0d30..ae63a3220f4 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc @@ -6,13 +6,20 @@ #include "flutter/runtime/dart_vm.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "tests/embedder_test_context.h" #include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/swiftshader/include/vulkan/vulkan_core.h" #ifdef SHELL_ENABLE_GL #include "flutter/shell/platform/embedder/tests/embedder_test_compositor_gl.h" #include "flutter/shell/platform/embedder/tests/embedder_test_context_gl.h" #endif +#ifdef SHELL_ENABLE_VULKAN +#include "flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h" +#include "flutter/vulkan/vulkan_device.h" +#endif + #ifdef SHELL_ENABLE_METAL #include "flutter/shell/platform/embedder/tests/embedder_test_context_metal.h" #endif @@ -73,6 +80,10 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( InitializeMetalRendererConfig(); #endif +#ifdef SHELL_ENABLE_VULKAN + InitializeVulkanRendererConfig(); +#endif + software_renderer_config_.struct_size = sizeof(FlutterSoftwareRendererConfig); software_renderer_config_.surface_present_callback = [](void* context, const void* allocation, size_t row_bytes, @@ -154,6 +165,24 @@ void EmbedderConfigBuilder::SetOpenGLPresentCallBack() { #endif } +void EmbedderConfigBuilder::SetRendererConfig(EmbedderTestContextType type, + SkISize surface_size) { + switch (type) { + case EmbedderTestContextType::kOpenGLContext: + SetOpenGLRendererConfig(surface_size); + break; + case EmbedderTestContextType::kMetalContext: + SetMetalRendererConfig(surface_size); + break; + case EmbedderTestContextType::kVulkanContext: + SetVulkanRendererConfig(surface_size); + break; + case EmbedderTestContextType::kSoftwareContext: + SetSoftwareRendererConfig(surface_size); + break; + } +} + void EmbedderConfigBuilder::SetOpenGLRendererConfig(SkISize surface_size) { #ifdef SHELL_ENABLE_GL renderer_config_.type = FlutterRendererType::kOpenGL; @@ -170,6 +199,14 @@ void EmbedderConfigBuilder::SetMetalRendererConfig(SkISize surface_size) { #endif } +void EmbedderConfigBuilder::SetVulkanRendererConfig(SkISize surface_size) { +#ifdef SHELL_ENABLE_VULKAN + renderer_config_.type = FlutterRendererType::kVulkan; + renderer_config_.vulkan = vulkan_renderer_config_; + context_.SetupSurface(surface_size); +#endif +} + void EmbedderConfigBuilder::SetAssetsPath() { project_args_.assets_path = context_.GetAssetsPath().c_str(); } @@ -428,5 +465,60 @@ void EmbedderConfigBuilder::InitializeMetalRendererConfig() { #endif // SHELL_ENABLE_METAL +#ifdef SHELL_ENABLE_VULKAN + +void EmbedderConfigBuilder::InitializeVulkanRendererConfig() { + if (context_.GetContextType() != EmbedderTestContextType::kVulkanContext) { + return; + } + + vulkan_renderer_config_.struct_size = sizeof(FlutterVulkanRendererConfig); + vulkan_renderer_config_.version = + static_cast(context_) + .vulkan_context_->application_->GetAPIVersion(); + vulkan_renderer_config_.instance = + static_cast(context_) + .vulkan_context_->application_->GetInstance(); + vulkan_renderer_config_.physical_device = + static_cast(context_) + .vulkan_context_->device_->GetPhysicalDeviceHandle(); + vulkan_renderer_config_.device = + static_cast(context_) + .vulkan_context_->device_->GetHandle(); + vulkan_renderer_config_.queue_family_index = + static_cast(context_) + .vulkan_context_->device_->GetGraphicsQueueIndex(); + vulkan_renderer_config_.queue = + static_cast(context_) + .vulkan_context_->device_->GetQueueHandle(); + vulkan_renderer_config_.get_instance_proc_address_callback = + [](void* context, FlutterVulkanInstanceHandle instance, + const char* name) -> void* { + return reinterpret_cast(context) + ->vulkan_context_->vk_->GetInstanceProcAddr( + reinterpret_cast(instance), name); + }; + vulkan_renderer_config_.get_next_image_callback = + [](void* context, + const FlutterFrameInfo* frame_info) -> FlutterVulkanImage { + VkImage image = + reinterpret_cast(context)->GetNextImage( + {static_cast(frame_info->size.width), + static_cast(frame_info->size.height)}); + return { + .struct_size = sizeof(FlutterVulkanImage), + .image = reinterpret_cast(image), + .format = VK_FORMAT_R8G8B8A8_UNORM, + }; + }; + vulkan_renderer_config_.present_image_callback = + [](void* context, const FlutterVulkanImage* image) -> bool { + return reinterpret_cast(context)->PresentImage( + reinterpret_cast(image->image)); + }; +} + +#endif + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h index ee1fb514bb6..73256a36b3e 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h @@ -45,12 +45,16 @@ class EmbedderConfigBuilder { FlutterProjectArgs& GetProjectArgs(); + void SetRendererConfig(EmbedderTestContextType type, SkISize surface_size); + void SetSoftwareRendererConfig(SkISize surface_size = SkISize::Make(1, 1)); void SetOpenGLRendererConfig(SkISize surface_size); void SetMetalRendererConfig(SkISize surface_size); + void SetVulkanRendererConfig(SkISize surface_size); + // Used to explicitly set an `open_gl.fbo_callback`. Using this method will // cause your test to fail since the ctor for this class sets // `open_gl.fbo_callback_with_frame_info`. This method exists as a utility to @@ -117,6 +121,10 @@ class EmbedderConfigBuilder { #ifdef SHELL_ENABLE_GL FlutterOpenGLRendererConfig opengl_renderer_config_ = {}; #endif +#ifdef SHELL_ENABLE_VULKAN + void InitializeVulkanRendererConfig(); + FlutterVulkanRendererConfig vulkan_renderer_config_ = {}; +#endif #ifdef SHELL_ENABLE_METAL void InitializeMetalRendererConfig(); FlutterMetalRendererConfig metal_renderer_config_ = {}; diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_test.cc index b816ab5d223..9a315dec3c4 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test.cc @@ -4,6 +4,7 @@ #include "flutter/shell/platform/embedder/tests/embedder_test.h" #include "flutter/shell/platform/embedder/tests/embedder_test_context_software.h" +#include "flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h" #ifdef SHELL_ENABLE_GL #include "flutter/shell/platform/embedder/tests/embedder_test_context_gl.h" @@ -33,6 +34,12 @@ EmbedderTestContext& EmbedderTest::GetEmbedderContext( std::make_unique( GetFixturesDirectory()); break; +#ifdef SHELL_ENABLE_VULKAN + case EmbedderTestContextType::kVulkanContext: + embedder_contexts_[type] = + std::make_unique(GetFixturesDirectory()); + break; +#endif #ifdef SHELL_ENABLE_GL case EmbedderTestContextType::kOpenGLContext: embedder_contexts_[type] = diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test.h index f00d802e0f2..eb166378728 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test.h @@ -12,6 +12,7 @@ #include "flutter/shell/platform/embedder/tests/embedder_test_context.h" #include "flutter/testing/testing.h" #include "flutter/testing/thread_test.h" +#include "gtest/gtest.h" namespace flutter { namespace testing { @@ -31,6 +32,10 @@ class EmbedderTest : public ThreadTest { FML_DISALLOW_COPY_AND_ASSIGN(EmbedderTest); }; +class EmbedderTestMultiBackend + : public EmbedderTest, + public ::testing::WithParamInterface {}; + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.cc index 6d0c6b9d254..94fa852d10c 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.cc @@ -5,8 +5,8 @@ #include "flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h" #include "flutter/fml/logging.h" -#include "include/core/SkImageInfo.h" -#include "include/core/SkSize.h" +#include "third_party/skia/include/core/SkImageInfo.h" +#include "third_party/skia/include/core/SkSize.h" #include "third_party/skia/include/core/SkSurface.h" #include @@ -23,6 +23,10 @@ EmbedderTestBackingStoreProducer::EmbedderTestBackingStoreProducer( , test_metal_context_(std::make_unique()) #endif +#ifdef SHELL_ENABLE_VULKAN + , + test_vulkan_context_(nullptr) +#endif { } @@ -43,6 +47,10 @@ bool EmbedderTestBackingStoreProducer::Create( #ifdef SHELL_ENABLE_METAL case RenderTargetType::kMetalTexture: return CreateMTLTexture(config, renderer_out); +#endif +#ifdef SHELL_ENABLE_VULKAN + case RenderTargetType::kVulkanImage: + return CreateVulkanImage(config, renderer_out); #endif default: return false; @@ -229,5 +237,86 @@ bool EmbedderTestBackingStoreProducer::CreateMTLTexture( #endif } +bool EmbedderTestBackingStoreProducer::CreateVulkanImage( + const FlutterBackingStoreConfig* config, + FlutterBackingStore* backing_store_out) { +#ifdef SHELL_ENABLE_VULKAN + if (!test_vulkan_context_) { + test_vulkan_context_ = fml::MakeRefCounted(); + } + + auto surface_size = SkISize::Make(config->size.width, config->size.height); + TestVulkanImage* test_image = new TestVulkanImage( + std::move(test_vulkan_context_->CreateImage(surface_size).value())); + + GrVkImageInfo image_info = { + .fImage = test_image->GetImage(), + .fImageTiling = VK_IMAGE_TILING_OPTIMAL, + .fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .fFormat = VK_FORMAT_R8G8B8A8_UNORM, + .fImageUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, + .fSampleCount = 1, + .fLevelCount = 1, + }; + GrBackendTexture backend_texture(surface_size.width(), surface_size.height(), + image_info); + + SkSurfaceProps surface_properties(0, kUnknown_SkPixelGeometry); + + SkSurface::TextureReleaseProc release_vktexture = [](void* user_data) { + delete reinterpret_cast(user_data); + }; + + sk_sp surface = SkSurface::MakeFromBackendTexture( + context_.get(), // context + backend_texture, // back-end texture + kTopLeft_GrSurfaceOrigin, // surface origin + 1, // sample count + kRGBA_8888_SkColorType, // color type + SkColorSpace::MakeSRGB(), // color space + &surface_properties, // surface properties + release_vktexture, // texture release proc + test_image // release context + ); + + if (!surface) { + FML_LOG(ERROR) << "Could not create Skia surface from Vulkan image."; + return false; + } + backing_store_out->type = kFlutterBackingStoreTypeVulkan; + + FlutterVulkanImage* image = new FlutterVulkanImage(); + image->image = reinterpret_cast(image_info.fImage); + image->format = VK_FORMAT_R8G8B8A8_UNORM; + backing_store_out->vulkan.image = image; + + // Collect all allocated resources in the destruction_callback. + { + UserData* user_data = new UserData(); + user_data->image = image; + user_data->surface = surface.get(); + + backing_store_out->user_data = user_data; + backing_store_out->vulkan.user_data = user_data; + backing_store_out->vulkan.destruction_callback = [](void* user_data) { + UserData* d = reinterpret_cast(user_data); + d->surface->unref(); + delete d->image; + delete d; + }; + + // The balancing unref is in the destruction callback. + surface->ref(); + } + + return true; +#else + return false; +#endif +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h index f993891e444..956c565d29f 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h @@ -7,6 +7,7 @@ #include #include "flutter/fml/macros.h" +#include "flutter/fml/memory/ref_ptr_internal.h" #include "flutter/shell/platform/embedder/embedder.h" #include "third_party/skia/include/gpu/GrDirectContext.h" @@ -14,16 +15,26 @@ #include "flutter/testing/test_metal_context.h" #endif +#ifdef SHELL_ENABLE_VULKAN +#include "flutter/testing/test_vulkan_context.h" +#endif + namespace flutter { namespace testing { class EmbedderTestBackingStoreProducer { public: + struct UserData { + SkSurface* surface; + FlutterVulkanImage* image; + }; + enum class RenderTargetType { kSoftwareBuffer, kOpenGLFramebuffer, kOpenGLTexture, kMetalTexture, + kVulkanImage, }; EmbedderTestBackingStoreProducer(sk_sp context, @@ -46,6 +57,9 @@ class EmbedderTestBackingStoreProducer { bool CreateMTLTexture(const FlutterBackingStoreConfig* config, FlutterBackingStore* renderer_out); + bool CreateVulkanImage(const FlutterBackingStoreConfig* config, + FlutterBackingStore* renderer_out); + sk_sp context_; RenderTargetType type_; @@ -53,6 +67,10 @@ class EmbedderTestBackingStoreProducer { std::unique_ptr test_metal_context_; #endif +#ifdef SHELL_ENABLE_VULKAN + fml::RefPtr test_vulkan_context_; +#endif + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderTestBackingStoreProducer); }; diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.cc new file mode 100644 index 00000000000..a932260513b --- /dev/null +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.cc @@ -0,0 +1,111 @@ +// 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. + +#include "flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.h" + +#include "flutter/fml/logging.h" +#include "flutter/shell/platform/embedder/tests/embedder_assertions.h" +#include "flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h" +#include "third_party/skia/include/core/SkSurface.h" + +namespace flutter { +namespace testing { + +EmbedderTestCompositorVulkan::EmbedderTestCompositorVulkan( + SkISize surface_size, + sk_sp context) + : EmbedderTestCompositor(surface_size, context) {} + +EmbedderTestCompositorVulkan::~EmbedderTestCompositorVulkan() = default; + +bool EmbedderTestCompositorVulkan::UpdateOffscrenComposition( + const FlutterLayer** layers, + size_t layers_count) { + last_composition_ = nullptr; + + const auto image_info = SkImageInfo::MakeN32Premul(surface_size_); + + sk_sp surface = + SkSurface::MakeRenderTarget(context_.get(), // context + SkBudgeted::kNo, // budgeted + image_info, // image info + 1, // sample count + kTopLeft_GrSurfaceOrigin, // surface origin + nullptr, // surface properties + false // create mipmaps + ); + + if (!surface) { + FML_LOG(ERROR) << "Could not update the off-screen composition."; + return false; + } + + auto canvas = surface->getCanvas(); + + // This has to be transparent because we are going to be compositing this + // sub-hierarchy onto the on-screen surface. + canvas->clear(SK_ColorTRANSPARENT); + + for (size_t i = 0; i < layers_count; ++i) { + const auto* layer = layers[i]; + + sk_sp platform_rendered_contents; + + sk_sp layer_image; + SkIPoint canvas_offset = SkIPoint::Make(0, 0); + + switch (layer->type) { + case kFlutterLayerContentTypeBackingStore: + layer_image = + reinterpret_cast( + layer->backing_store->user_data) + ->surface->makeImageSnapshot(); + break; + case kFlutterLayerContentTypePlatformView: + layer_image = + platform_view_renderer_callback_ + ? platform_view_renderer_callback_(*layer, context_.get()) + : nullptr; + canvas_offset = SkIPoint::Make(layer->offset.x, layer->offset.y); + break; + }; + + // If the layer is not a platform view but the engine did not specify an + // image for the backing store, it is an error. + if (!layer_image && layer->type != kFlutterLayerContentTypePlatformView) { + FML_LOG(ERROR) << "Could not snapshot layer in test compositor: " + << *layer; + return false; + } + + // The test could have just specified no contents to be rendered in place of + // a platform view. This is not an error. + if (layer_image) { + // The image rendered by Flutter already has the correct offset and + // transformation applied. The layers offset is meant for the platform. + canvas->drawImage(layer_image.get(), canvas_offset.x(), + canvas_offset.y()); + } + } + + last_composition_ = surface->makeImageSnapshot(); + + if (!last_composition_) { + FML_LOG(ERROR) << "Could not update the contents of the sub-composition."; + return false; + } + + if (next_scene_callback_) { + auto last_composition_snapshot = last_composition_->makeRasterImage(); + FML_CHECK(last_composition_snapshot); + auto callback = next_scene_callback_; + next_scene_callback_ = nullptr; + callback(std::move(last_composition_snapshot)); + } + + return true; +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.h new file mode 100644 index 00000000000..608bda52ec8 --- /dev/null +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.h @@ -0,0 +1,32 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_TEST_COMPOSITOR_VULKAN_H_ +#define FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_TEST_COMPOSITOR_VULKAN_H_ + +#include "flutter/fml/macros.h" +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/embedder/tests/embedder_test_compositor.h" + +namespace flutter { +namespace testing { + +class EmbedderTestCompositorVulkan : public EmbedderTestCompositor { + public: + EmbedderTestCompositorVulkan(SkISize surface_size, + sk_sp context); + + ~EmbedderTestCompositorVulkan() override; + + private: + bool UpdateOffscrenComposition(const FlutterLayer** layers, + size_t layers_count) override; + + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderTestCompositorVulkan); +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_TEST_COMPOSITOR_VULKAN_H_ diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h index 3f4f6d06b9c..f7df168122a 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h @@ -44,6 +44,7 @@ enum class EmbedderTestContextType { kSoftwareContext, kOpenGLContext, kMetalContext, + kVulkanContext, }; class EmbedderTestContext { @@ -105,6 +106,12 @@ class EmbedderTestContext { using NextSceneCallback = std::function image)>; +#ifdef SHELL_ENABLE_VULKAN + // The TestVulkanContext destructor must be called _after_ the compositor is + // freed. + fml::RefPtr vulkan_context_ = nullptr; +#endif + std::string assets_path_; ELFAOTSymbols aot_symbols_; std::unique_ptr vm_snapshot_data_; diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.cc new file mode 100644 index 00000000000..0fb93fa1e36 --- /dev/null +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.cc @@ -0,0 +1,61 @@ +// 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. + +#include "flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h" + +#include + +#include "flutter/fml/logging.h" +#include "flutter/shell/platform/embedder/tests/embedder_test_compositor_vulkan.h" +#include "flutter/testing/test_vulkan_context.h" +#include "flutter/testing/test_vulkan_surface.h" +#include "flutter/vulkan/vulkan_device.h" +#include "flutter/vulkan/vulkan_proc_table.h" +#include "third_party/skia/include/core/SkSurface.h" + +namespace flutter { +namespace testing { + +EmbedderTestContextVulkan::EmbedderTestContextVulkan(std::string assets_path) + : EmbedderTestContext(assets_path), surface_() { + vulkan_context_ = fml::MakeRefCounted(); +} + +EmbedderTestContextVulkan::~EmbedderTestContextVulkan() {} + +void EmbedderTestContextVulkan::SetupSurface(SkISize surface_size) { + FML_CHECK(surface_size_.isEmpty()); + surface_size_ = surface_size; + surface_ = TestVulkanSurface::Create(*vulkan_context_, surface_size_); +} + +size_t EmbedderTestContextVulkan::GetSurfacePresentCount() const { + return present_count_; +} + +VkImage EmbedderTestContextVulkan::GetNextImage(const SkISize& size) { + return surface_->GetImage(); +} + +bool EmbedderTestContextVulkan::PresentImage(VkImage image) { + FireRootSurfacePresentCallbackIfPresent( + [&]() { return surface_->GetSurfaceSnapshot(); }); + present_count_++; + return true; +} + +EmbedderTestContextType EmbedderTestContextVulkan::GetContextType() const { + return EmbedderTestContextType::kVulkanContext; +} + +void EmbedderTestContextVulkan::SetupCompositor() { + FML_CHECK(!compositor_) << "Already set up a compositor in this context."; + FML_CHECK(surface_) + << "Set up the Vulkan surface before setting up a compositor."; + compositor_ = std::make_unique( + surface_size_, vulkan_context_->GetGrDirectContext()); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h new file mode 100644 index 00000000000..b08aa439417 --- /dev/null +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context_vulkan.h @@ -0,0 +1,52 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_CONTEXT_VULKAN_H_ +#define FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_CONTEXT_VULKAN_H_ + +#include +#include "flutter/shell/platform/embedder/tests/embedder_test_context.h" +#include "flutter/testing/test_vulkan_context.h" +#include "flutter/vulkan/vulkan_application.h" +#include "testing/test_vulkan_surface.h" + +namespace flutter { +namespace testing { + +class EmbedderTestContextVulkan : public EmbedderTestContext { + public: + explicit EmbedderTestContextVulkan(std::string assets_path = ""); + + ~EmbedderTestContextVulkan() override; + + // |EmbedderTestContext| + EmbedderTestContextType GetContextType() const override; + + // |EmbedderTestContext| + size_t GetSurfacePresentCount() const override; + + // |EmbedderTestContext| + void SetupCompositor() override; + + VkImage GetNextImage(const SkISize& size); + + bool PresentImage(VkImage image); + + private: + std::unique_ptr surface_; + + SkISize surface_size_ = SkISize::MakeEmpty(); + size_t present_count_ = 0; + + void SetupSurface(SkISize surface_size) override; + + friend class EmbedderConfigBuilder; + + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderTestContextVulkan); +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_EMBEDDER_TESTS_EMBEDDER_CONTEXT_VULKAN_H_ diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_gl.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_gl.cc index c8086191c8b..94f2eddb42f 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_gl.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_gl.cc @@ -2,19 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "tests/embedder_test_context.h" #define FML_USED_ON_EMBEDDER #include #include -#include "embedder.h" -#include "embedder_engine.h" +#include "vulkan/vulkan.h" + #include "flutter/flow/raster_cache.h" #include "flutter/fml/file.h" #include "flutter/fml/make_copyable.h" #include "flutter/fml/mapping.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/message_loop_task_queues.h" +#include "flutter/fml/native_library.h" #include "flutter/fml/paths.h" #include "flutter/fml/synchronization/count_down_latch.h" #include "flutter/fml/synchronization/waitable_event.h" @@ -29,7 +31,6 @@ #include "flutter/shell/platform/embedder/tests/embedder_unittests_util.h" #include "flutter/testing/assertions_skia.h" #include "flutter/testing/test_gl_surface.h" -#include "flutter/testing/test_vulkan_context.h" #include "flutter/testing/testing.h" #include "third_party/skia/include/core/SkSurface.h" #include "third_party/skia/src/gpu/gl/GrGLDefines.h" @@ -40,14 +41,9 @@ namespace testing { using EmbedderTest = testing::EmbedderTest; -//------------------------------------------------------------------------------ -/// This is a sanity check to ensure Swiftshader Vulkan is working. Once Vulkan -/// support lands in the embedder API, it'll be tested via a new -/// EmbedderTestContext type/config. -/// -TEST_F(EmbedderTest, CanInitializeTestVulkanContext) { - TestVulkanContext ctx; - ASSERT_TRUE(ctx.IsValid()); +TEST_F(EmbedderTest, CanGetVulkanEmbedderContext) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kVulkanContext); + EmbedderConfigBuilder builder(context); } TEST_F(EmbedderTest, CanCreateOpenGLRenderingEngine) { @@ -1238,13 +1234,14 @@ TEST_F(EmbedderTest, CanRenderSceneWithoutCustomCompositorWithTransformation) { "scene_without_custom_compositor_with_xform.png", rendered_scene)); } -TEST_F(EmbedderTest, CanRenderGradientWithoutCompositor) { - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); +TEST_P(EmbedderTestMultiBackend, CanRenderGradientWithoutCompositor) { + EmbedderTestContextType backend = GetParam(); + auto& context = GetEmbedderContext(backend); EmbedderConfigBuilder builder(context); builder.SetDartEntrypoint("render_gradient"); - builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetRendererConfig(backend, SkISize::Make(800, 600)); auto rendered_scene = context.GetNextSceneImage(); @@ -1260,7 +1257,8 @@ TEST_F(EmbedderTest, CanRenderGradientWithoutCompositor) { ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); - ASSERT_TRUE(ImageMatchesFixture("gradient.png", rendered_scene)); + ASSERT_TRUE(ImageMatchesFixture( + FixtureNameForBackend(backend, "gradient.png"), rendered_scene)); } TEST_F(EmbedderTest, CanRenderGradientWithoutCompositorWithXform) { @@ -1296,16 +1294,16 @@ TEST_F(EmbedderTest, CanRenderGradientWithoutCompositorWithXform) { ASSERT_TRUE(ImageMatchesFixture("gradient_xform.png", rendered_scene)); } -TEST_F(EmbedderTest, CanRenderGradientWithCompositor) { - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); +TEST_P(EmbedderTestMultiBackend, CanRenderGradientWithCompositor) { + EmbedderTestContextType backend = GetParam(); + auto& context = GetEmbedderContext(backend); EmbedderConfigBuilder builder(context); builder.SetDartEntrypoint("render_gradient"); - builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetRendererConfig(backend, SkISize::Make(800, 600)); builder.SetCompositor(); - builder.SetRenderTargetType( - EmbedderTestBackingStoreProducer::RenderTargetType::kOpenGLFramebuffer); + builder.SetRenderTargetType(GetRenderTargetFromBackend(backend, true)); auto rendered_scene = context.GetNextSceneImage(); @@ -1321,7 +1319,8 @@ TEST_F(EmbedderTest, CanRenderGradientWithCompositor) { ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); - ASSERT_TRUE(ImageMatchesFixture("gradient.png", rendered_scene)); + ASSERT_TRUE(ImageMatchesFixture( + FixtureNameForBackend(backend, "gradient.png"), rendered_scene)); } TEST_F(EmbedderTest, CanRenderGradientWithCompositorWithXform) { @@ -1361,16 +1360,17 @@ TEST_F(EmbedderTest, CanRenderGradientWithCompositorWithXform) { ASSERT_TRUE(ImageMatchesFixture("gradient_xform.png", rendered_scene)); } -TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayer) { - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); +TEST_P(EmbedderTestMultiBackend, + CanRenderGradientWithCompositorOnNonRootLayer) { + EmbedderTestContextType backend = GetParam(); + auto& context = GetEmbedderContext(backend); EmbedderConfigBuilder builder(context); builder.SetDartEntrypoint("render_gradient_on_non_root_backing_store"); - builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetRendererConfig(backend, SkISize::Make(800, 600)); builder.SetCompositor(); - builder.SetRenderTargetType( - EmbedderTestBackingStoreProducer::RenderTargetType::kOpenGLFramebuffer); + builder.SetRenderTargetType(GetRenderTargetFromBackend(backend, true)); context.GetCompositor().SetNextPresentCallback( [&](const FlutterLayer** layers, size_t layers_count) { @@ -1379,9 +1379,8 @@ TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayer) { // Layer Root { FlutterBackingStore backing_store = *layers[0]->backing_store; - backing_store.type = kFlutterBackingStoreTypeOpenGL; backing_store.did_update = true; - backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + ConfigureBackingStore(backing_store, backend, true); FlutterLayer layer = {}; layer.struct_size = sizeof(layer); @@ -1412,9 +1411,8 @@ TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayer) { // Layer 2 { FlutterBackingStore backing_store = *layers[2]->backing_store; - backing_store.type = kFlutterBackingStoreTypeOpenGL; backing_store.did_update = true; - backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + ConfigureBackingStore(backing_store, backend, true); FlutterLayer layer = {}; layer.struct_size = sizeof(layer); @@ -1463,7 +1461,8 @@ TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayer) { ASSERT_EQ(FlutterEngineSendWindowMetricsEvent(engine.get(), &event), kSuccess); - ASSERT_TRUE(ImageMatchesFixture("gradient.png", rendered_scene)); + ASSERT_TRUE(ImageMatchesFixture( + FixtureNameForBackend(backend, "gradient.png"), rendered_scene)); } TEST_F(EmbedderTest, CanRenderGradientWithCompositorOnNonRootLayerWithXform) { @@ -1738,7 +1737,7 @@ TEST_F(EmbedderTest, CanCreateEmbedderWithCustomRenderTaskRunner) { /// Asserts that the render task runner can be the same as the platform task /// runner. /// -TEST_F(EmbedderTest, +TEST_P(EmbedderTestMultiBackend, CanCreateEmbedderWithCustomRenderTaskRunnerTheSameAsPlatformTaskRunner) { // A new thread needs to be created for the platform thread because the test // can't wait for assertions to be completed on the same thread that services @@ -1760,10 +1759,10 @@ TEST_F(EmbedderTest, }); platform_task_runner->PostTask([&]() { - EmbedderConfigBuilder builder( - GetEmbedderContext(EmbedderTestContextType::kOpenGLContext)); + EmbedderTestContextType backend = GetParam(); + EmbedderConfigBuilder builder(GetEmbedderContext(backend)); builder.SetDartEntrypoint("can_render_scene_without_custom_compositor"); - builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetRendererConfig(backend, SkISize::Make(800, 600)); builder.SetRenderTaskRunner( &common_task_runner.GetFlutterTaskRunnerDescription()); builder.SetPlatformTaskRunner( @@ -1813,17 +1812,17 @@ TEST_F(EmbedderTest, } } -TEST_F(EmbedderTest, +TEST_P(EmbedderTestMultiBackend, CompositorMustBeAbleToRenderKnownScenePixelRatioOnSurface) { - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); + EmbedderTestContextType backend = GetParam(); + auto& context = GetEmbedderContext(backend); EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetRendererConfig(backend, SkISize::Make(800, 600)); builder.SetCompositor(); builder.SetDartEntrypoint("can_display_platform_view_with_pixel_ratio"); - builder.SetRenderTargetType( - EmbedderTestBackingStoreProducer::RenderTargetType::kOpenGLTexture); + builder.SetRenderTargetType(GetRenderTargetFromBackend(backend, false)); fml::CountDownLatch latch(1); @@ -1836,9 +1835,8 @@ TEST_F(EmbedderTest, // Layer 0 (Root) { FlutterBackingStore backing_store = *layers[0]->backing_store; - backing_store.type = kFlutterBackingStoreTypeOpenGL; backing_store.did_update = true; - backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + ConfigureBackingStore(backing_store, backend, false); FlutterLayer layer = {}; layer.struct_size = sizeof(layer); @@ -1869,9 +1867,8 @@ TEST_F(EmbedderTest, // Layer 2 { FlutterBackingStore backing_store = *layers[2]->backing_store; - backing_store.type = kFlutterBackingStoreTypeOpenGL; backing_store.did_update = true; - backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + ConfigureBackingStore(backing_store, backend, false); FlutterLayer layer = {}; layer.struct_size = sizeof(layer); @@ -1900,7 +1897,8 @@ TEST_F(EmbedderTest, latch.Wait(); - ASSERT_TRUE(ImageMatchesFixture("dpr_noxform.png", rendered_scene)); + ASSERT_TRUE(ImageMatchesFixture( + FixtureNameForBackend(backend, "dpr_noxform.png"), rendered_scene)); } TEST_F( @@ -2084,16 +2082,16 @@ TEST_F(EmbedderTest, FlutterEngineShutdown(engine.release()); } -TEST_F(EmbedderTest, PlatformViewMutatorsAreValid) { - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); +TEST_P(EmbedderTestMultiBackend, PlatformViewMutatorsAreValid) { + EmbedderTestContextType backend = GetParam(); + auto& context = GetEmbedderContext(backend); EmbedderConfigBuilder builder(context); - builder.SetOpenGLRendererConfig(SkISize::Make(800, 600)); + builder.SetRendererConfig(backend, SkISize::Make(800, 600)); builder.SetCompositor(); builder.SetDartEntrypoint("platform_view_mutators"); - builder.SetRenderTargetType( - EmbedderTestBackingStoreProducer::RenderTargetType::kOpenGLTexture); + builder.SetRenderTargetType(GetRenderTargetFromBackend(backend, false)); fml::CountDownLatch latch(1); context.GetCompositor().SetNextPresentCallback( @@ -2103,9 +2101,8 @@ TEST_F(EmbedderTest, PlatformViewMutatorsAreValid) { // Layer 0 (Root) { FlutterBackingStore backing_store = *layers[0]->backing_store; - backing_store.type = kFlutterBackingStoreTypeOpenGL; backing_store.did_update = true; - backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + ConfigureBackingStore(backing_store, backend, false); FlutterLayer layer = {}; layer.struct_size = sizeof(layer); @@ -3691,5 +3688,11 @@ TEST_F(EmbedderTest, ExternalTextureGLRefreshedTooOften) { EXPECT_TRUE(resolve_called); } +INSTANTIATE_TEST_SUITE_P( + EmbedderTestGlVk, + EmbedderTestMultiBackend, + ::testing::Values(EmbedderTestContextType::kOpenGLContext, + EmbedderTestContextType::kVulkanContext)); + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.cc index 89d5fec9956..d076dc1c017 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.cc @@ -6,6 +6,7 @@ #include +#include "flutter/shell/platform/embedder/tests/embedder_test_backingstore_producer.h" #include "flutter/shell/platform/embedder/tests/embedder_unittests_util.h" namespace flutter { @@ -69,6 +70,61 @@ bool RasterImagesAreSame(sk_sp a, sk_sp b) { return normalized_a->equals(normalized_b.get()); } +std::string FixtureNameForBackend(EmbedderTestContextType backend, + const std::string& name) { + switch (backend) { + case EmbedderTestContextType::kVulkanContext: + return "vk_" + name; + default: + return name; + } +} + +EmbedderTestBackingStoreProducer::RenderTargetType GetRenderTargetFromBackend( + EmbedderTestContextType backend, + bool opengl_framebuffer) { + switch (backend) { + case EmbedderTestContextType::kVulkanContext: + return EmbedderTestBackingStoreProducer::RenderTargetType::kVulkanImage; + case EmbedderTestContextType::kOpenGLContext: + if (opengl_framebuffer) { + return EmbedderTestBackingStoreProducer::RenderTargetType:: + kOpenGLFramebuffer; + } + return EmbedderTestBackingStoreProducer::RenderTargetType::kOpenGLTexture; + case EmbedderTestContextType::kMetalContext: + return EmbedderTestBackingStoreProducer::RenderTargetType::kMetalTexture; + case EmbedderTestContextType::kSoftwareContext: + return EmbedderTestBackingStoreProducer::RenderTargetType:: + kSoftwareBuffer; + } +} + +void ConfigureBackingStore(FlutterBackingStore& backing_store, + EmbedderTestContextType backend, + bool opengl_framebuffer) { + switch (backend) { + case EmbedderTestContextType::kVulkanContext: + backing_store.type = kFlutterBackingStoreTypeVulkan; + break; + case EmbedderTestContextType::kOpenGLContext: + if (opengl_framebuffer) { + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeFramebuffer; + } else { + backing_store.type = kFlutterBackingStoreTypeOpenGL; + backing_store.open_gl.type = kFlutterOpenGLTargetTypeTexture; + } + break; + case EmbedderTestContextType::kMetalContext: + backing_store.type = kFlutterBackingStoreTypeMetal; + break; + case EmbedderTestContextType::kSoftwareContext: + backing_store.type = kFlutterBackingStoreTypeSoftware; + break; + } +} + bool WriteImageToDisk(const fml::UniqueFD& directory, const std::string& name, sk_sp image) { diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.h index 35944cb13ad..f02c1d61c0c 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests_util.h @@ -9,7 +9,6 @@ #include -#include "embedder.h" #include "flutter/fml/mapping.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/paths.h" @@ -26,6 +25,40 @@ sk_sp CreateRenderSurface(const FlutterLayer& layer, bool RasterImagesAreSame(sk_sp a, sk_sp b); +/// @brief Prepends a prefix to the name which is unique to the test +/// context type. This is useful for tests that use +/// EmbedderTestMultiBackend and require different fixtures per +/// backend. For OpenGL, the name remains unchanged. +/// @param[in] backend The test context type used to determine the prepended +/// prefix (e.g. `vk_[name]` for Vulkan). +/// @param[in] name The name of the fixture without any special prefixes. +std::string FixtureNameForBackend(EmbedderTestContextType backend, + const std::string& name); + +/// @brief Resolves a render target type for a given backend description. +/// This is useful for tests that use EmbedderTestMultiBackend. +/// @param[in] backend The test context type to resolve the render +/// target for. +/// @param[in] opengl_framebuffer Ignored for all non-OpenGL backends. Flutter +/// supports rendering to both OpenGL textures +/// and framebuffers. When false, the OpenGL +/// texture render target type is returned. +EmbedderTestBackingStoreProducer::RenderTargetType GetRenderTargetFromBackend( + EmbedderTestContextType backend, + bool opengl_framebuffer); + +/// @brief Configures per-backend properties for a given backing store. +/// @param[in] backing_store The backing store to configure. +/// @param[in] backend The test context type used to decide which +/// backend the backing store will be used with. +/// @param[in] opengl_framebuffer Ignored for all non-OpenGL backends. Flutter +/// supports rendering to both OpenGL textures +/// and framebuffers. When false, the backing +/// store is configured to be an OpenGL texture. +void ConfigureBackingStore(FlutterBackingStore& backing_store, + EmbedderTestContextType backend, + bool opengl_framebuffer); + bool WriteImageToDisk(const fml::UniqueFD& directory, const std::string& name, sk_sp image); diff --git a/engine/src/flutter/testing/BUILD.gn b/engine/src/flutter/testing/BUILD.gn index 9ae50f47e8d..3b47f5ac25d 100644 --- a/engine/src/flutter/testing/BUILD.gn +++ b/engine/src/flutter/testing/BUILD.gn @@ -128,6 +128,10 @@ if (enable_unittests) { sources = [ "test_vulkan_context.cc", "test_vulkan_context.h", + "test_vulkan_image.cc", + "test_vulkan_image.h", + "test_vulkan_surface.cc", + "test_vulkan_surface.h", ] defines = [ "TEST_VULKAN_PROCS" ] @@ -135,6 +139,7 @@ if (enable_unittests) { deps = [ ":skia", "//flutter/fml", + "//flutter/shell/common", "//flutter/vulkan", ] diff --git a/engine/src/flutter/testing/test_vulkan_context.cc b/engine/src/flutter/testing/test_vulkan_context.cc index 36c30989d08..56eeb558cc9 100644 --- a/engine/src/flutter/testing/test_vulkan_context.cc +++ b/engine/src/flutter/testing/test_vulkan_context.cc @@ -2,9 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "test_vulkan_context.h" +#include +#include -#include "flutter/vulkan/vulkan_proc_table.h" +#include "flutter/fml/logging.h" +#include "flutter/shell/common/context_options.h" +#include "flutter/testing/test_vulkan_context.h" + +#include "flutter/fml/memory/ref_ptr.h" +#include "flutter/fml/native_library.h" +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/gpu/GrDirectContext.h" +#include "third_party/skia/include/gpu/vk/GrVkExtensions.h" +#include "vulkan/vulkan_core.h" #ifdef FML_OS_MACOSX #define VULKAN_SO_PATH "libvk_swiftshader.dylib" @@ -15,38 +25,164 @@ #endif namespace flutter { +namespace testing { -TestVulkanContext::TestVulkanContext() : valid_(false) { - vk_ = fml::MakeRefCounted(VULKAN_SO_PATH); +TestVulkanContext::TestVulkanContext() { + // --------------------------------------------------------------------------- + // Initialize basic Vulkan state using the Swiftshader ICD. + // --------------------------------------------------------------------------- + + const char* vulkan_icd = VULKAN_SO_PATH; + + // TODO(96949): Clean this up and pass a native library directly to + // VulkanProcTable. + if (!fml::NativeLibrary::Create(VULKAN_SO_PATH)) { + FML_LOG(ERROR) << "Couldn't find Vulkan ICD \"" << vulkan_icd + << "\", trying \"libvulkan.so\" instead."; + vulkan_icd = "libvulkan.so"; + } + + FML_LOG(INFO) << "Using Vulkan ICD: " << vulkan_icd; + + vk_ = fml::MakeRefCounted(vulkan_icd); if (!vk_ || !vk_->HasAcquiredMandatoryProcAddresses()) { - FML_DLOG(ERROR) << "Proc table has not acquired mandatory proc addresses."; + FML_LOG(ERROR) << "Proc table has not acquired mandatory proc addresses."; return; } - application_ = std::unique_ptr( - new vulkan::VulkanApplication(*vk_, "Flutter Unittests", {})); + application_ = + std::unique_ptr(new vulkan::VulkanApplication( + *vk_, "Flutter Unittests", {}, VK_MAKE_VERSION(1, 0, 0), + VK_MAKE_VERSION(1, 0, 0), true)); if (!application_->IsValid()) { - FML_DLOG(ERROR) << "Failed to initialize basic Vulkan state."; + FML_LOG(ERROR) << "Failed to initialize basic Vulkan state."; return; } if (!vk_->AreInstanceProcsSetup()) { - FML_DLOG(ERROR) << "Failed to acquire full proc table."; + FML_LOG(ERROR) << "Failed to acquire full proc table."; return; } - logical_device_ = application_->AcquireFirstCompatibleLogicalDevice(); - if (!logical_device_ || !logical_device_->IsValid()) { - FML_DLOG(ERROR) << "Failed to create compatible logical device."; + device_ = application_->AcquireFirstCompatibleLogicalDevice(); + if (!device_ || !device_->IsValid()) { + FML_LOG(ERROR) << "Failed to create compatible logical device."; return; } - valid_ = true; + // --------------------------------------------------------------------------- + // Create a Skia context. + // For creating SkSurfaces from VkImages and snapshotting them, etc. + // --------------------------------------------------------------------------- + + uint32_t skia_features = 0; + if (!device_->GetPhysicalDeviceFeaturesSkia(&skia_features)) { + FML_LOG(ERROR) << "Failed to get physical device features."; + + return; + } + + auto get_proc = vk_->CreateSkiaGetProc(); + if (get_proc == nullptr) { + FML_LOG(ERROR) << "Failed to create Vulkan getProc for Skia."; + return; + } + + GrVkExtensions extensions; + + GrVkBackendContext backend_context = {}; + backend_context.fInstance = application_->GetInstance(); + backend_context.fPhysicalDevice = device_->GetPhysicalDeviceHandle(); + backend_context.fDevice = device_->GetHandle(); + backend_context.fQueue = device_->GetQueueHandle(); + backend_context.fGraphicsQueueIndex = device_->GetGraphicsQueueIndex(); + backend_context.fMinAPIVersion = VK_MAKE_VERSION(1, 0, 0); + backend_context.fMaxAPIVersion = VK_MAKE_VERSION(1, 0, 0); + backend_context.fFeatures = skia_features; + backend_context.fVkExtensions = &extensions; + backend_context.fGetProc = get_proc; + backend_context.fOwnsInstanceAndDevice = false; + + GrContextOptions options = + MakeDefaultContextOptions(ContextType::kRender, GrBackendApi::kVulkan); + options.fReduceOpsTaskSplitting = GrContextOptions::Enable::kNo; + context_ = GrDirectContext::MakeVulkan(backend_context, options); } -TestVulkanContext::~TestVulkanContext() = default; - -bool TestVulkanContext::IsValid() { - return valid_; +TestVulkanContext::~TestVulkanContext() { + if (context_) { + context_->releaseResourcesAndAbandonContext(); + } } +std::optional TestVulkanContext::CreateImage( + const SkISize& size) const { + TestVulkanImage result; + + VkImageCreateInfo info = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .extent = VkExtent3D{static_cast(size.width()), + static_cast(size.height()), 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + + VkImage image; + if (VK_CALL_LOG_ERROR(VK_CALL_LOG_ERROR( + vk_->CreateImage(device_->GetHandle(), &info, nullptr, &image)))) { + return std::nullopt; + } + + result.image_ = vulkan::VulkanHandle( + image, [&vk = vk_, &device = device_](VkImage image) { + vk->DestroyImage(device->GetHandle(), image, nullptr); + }); + + VkMemoryRequirements mem_req; + vk_->GetImageMemoryRequirements(device_->GetHandle(), image, &mem_req); + VkMemoryAllocateInfo alloc_info{}; + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.allocationSize = mem_req.size; + alloc_info.memoryTypeIndex = static_cast(__builtin_ctz( + mem_req.memoryTypeBits & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)); + + VkDeviceMemory memory; + if (VK_CALL_LOG_ERROR(vk_->AllocateMemory(device_->GetHandle(), &alloc_info, + nullptr, &memory)) != VK_SUCCESS) { + return std::nullopt; + } + + result.memory_ = vulkan::VulkanHandle{ + memory, [&vk = vk_, &device = device_](VkDeviceMemory memory) { + vk->FreeMemory(device->GetHandle(), memory, nullptr); + }}; + + if (VK_CALL_LOG_ERROR(VK_CALL_LOG_ERROR(vk_->BindImageMemory( + device_->GetHandle(), result.image_, result.memory_, 0)))) { + return std::nullopt; + } + + result.context_ = + fml::RefPtr(const_cast(this)); + + return result; +} + +sk_sp TestVulkanContext::GetGrDirectContext() const { + return context_; +} + +} // namespace testing } // namespace flutter diff --git a/engine/src/flutter/testing/test_vulkan_context.h b/engine/src/flutter/testing/test_vulkan_context.h index 5d79971809d..25d212bf05e 100644 --- a/engine/src/flutter/testing/test_vulkan_context.h +++ b/engine/src/flutter/testing/test_vulkan_context.h @@ -5,29 +5,44 @@ #ifndef FLUTTER_TESTING_TEST_VULKAN_CONTEXT_H_ #define FLUTTER_TESTING_TEST_VULKAN_CONTEXT_H_ +#include "flutter/fml/macros.h" +#include "flutter/fml/memory/ref_ptr.h" +#include "flutter/testing/test_vulkan_image.h" #include "flutter/vulkan/vulkan_application.h" #include "flutter/vulkan/vulkan_device.h" #include "flutter/vulkan/vulkan_proc_table.h" -namespace flutter { +#include "third_party/skia/include/core/SkSize.h" +#include "third_party/skia/include/gpu/GrDirectContext.h" -/// @brief Utility class to create a Vulkan device context, a corresponding -/// Skia context, and device resources. -class TestVulkanContext { +namespace flutter { +namespace testing { + +class TestVulkanContext : public fml::RefCountedThreadSafe { public: TestVulkanContext(); ~TestVulkanContext(); - bool IsValid(); + + std::optional CreateImage(const SkISize& size) const; + + sk_sp GetGrDirectContext() const; private: - bool valid_ = false; fml::RefPtr vk_; std::unique_ptr application_; - std::unique_ptr logical_device_; + std::unique_ptr device_; + sk_sp context_; + + friend class EmbedderTestContextVulkan; + friend class EmbedderConfigBuilder; + + FML_FRIEND_MAKE_REF_COUNTED(TestVulkanContext); + FML_FRIEND_REF_COUNTED_THREAD_SAFE(TestVulkanContext); FML_DISALLOW_COPY_AND_ASSIGN(TestVulkanContext); }; +} // namespace testing } // namespace flutter #endif // FLUTTER_TESTING_TEST_VULKAN_CONTEXT_H_ diff --git a/engine/src/flutter/testing/test_vulkan_image.cc b/engine/src/flutter/testing/test_vulkan_image.cc new file mode 100644 index 00000000000..4c365412ae6 --- /dev/null +++ b/engine/src/flutter/testing/test_vulkan_image.cc @@ -0,0 +1,24 @@ +// 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. + +#include "flutter/testing/test_vulkan_image.h" + +#include "flutter/testing/test_vulkan_context.h" + +namespace flutter { +namespace testing { + +TestVulkanImage::TestVulkanImage() = default; + +TestVulkanImage::TestVulkanImage(TestVulkanImage&& other) = default; +TestVulkanImage& TestVulkanImage::operator=(TestVulkanImage&& other) = default; + +TestVulkanImage::~TestVulkanImage() = default; + +VkImage TestVulkanImage::GetImage() { + return image_; +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/testing/test_vulkan_image.h b/engine/src/flutter/testing/test_vulkan_image.h new file mode 100644 index 00000000000..c8a6a797f38 --- /dev/null +++ b/engine/src/flutter/testing/test_vulkan_image.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef FLUTTER_TESTING_TEST_VULKAN_IMAGE_H_ +#define FLUTTER_TESTING_TEST_VULKAN_IMAGE_H_ + +#include "flutter/fml/macros.h" +#include "flutter/vulkan/vulkan_handle.h" + +#include "flutter/fml/memory/ref_ptr.h" +#include "third_party/skia/include/core/SkSize.h" + +namespace flutter { +namespace testing { + +class TestVulkanContext; + +/// Captures the lifetime of a test VkImage along with its bound memory. +class TestVulkanImage { + public: + TestVulkanImage(TestVulkanImage&& other); + TestVulkanImage& operator=(TestVulkanImage&& other); + + ~TestVulkanImage(); + + VkImage GetImage(); + + private: + TestVulkanImage(); + + // The lifetime of the Vulkan state must exceed memory/image handles. + fml::RefPtr context_; + + vulkan::VulkanHandle image_; + vulkan::VulkanHandle memory_; + + FML_DISALLOW_COPY_AND_ASSIGN(TestVulkanImage); + + friend TestVulkanContext; +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_TESTING_TEST_VULKAN_IMAGE_H_ diff --git a/engine/src/flutter/testing/test_vulkan_surface.cc b/engine/src/flutter/testing/test_vulkan_surface.cc new file mode 100644 index 00000000000..6bca8ed44f5 --- /dev/null +++ b/engine/src/flutter/testing/test_vulkan_surface.cc @@ -0,0 +1,110 @@ +// 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. + +#include "flutter/testing/test_vulkan_surface.h" +#include +#include "flutter/fml/logging.h" +#include "flutter/testing/test_vulkan_context.h" + +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/core/SkSurfaceProps.h" + +namespace flutter { +namespace testing { + +TestVulkanSurface::TestVulkanSurface(TestVulkanImage&& image) + : image_(std::move(image)){}; + +std::unique_ptr TestVulkanSurface::Create( + const TestVulkanContext& context, + const SkISize& surface_size) { + auto image_result = context.CreateImage(surface_size); + + if (!image_result.has_value()) { + FML_LOG(ERROR) << "Could not create VkImage."; + return nullptr; + } + + GrVkImageInfo image_info = { + .fImage = image_result.value().GetImage(), + .fImageTiling = VK_IMAGE_TILING_OPTIMAL, + .fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .fFormat = VK_FORMAT_R8G8B8A8_UNORM, + .fImageUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, + .fSampleCount = 1, + .fLevelCount = 1, + }; + GrBackendTexture backend_texture(surface_size.width(), // + surface_size.height(), // + image_info // + ); + + SkSurfaceProps surface_properties(0, kUnknown_SkPixelGeometry); + + auto result = std::unique_ptr( + new TestVulkanSurface(std::move(image_result.value()))); + result->surface_ = SkSurface::MakeFromBackendTexture( + context.GetGrDirectContext().get(), // context + backend_texture, // back-end texture + kTopLeft_GrSurfaceOrigin, // surface origin + 1, // sample count + kRGBA_8888_SkColorType, // color type + SkColorSpace::MakeSRGB(), // color space + &surface_properties, // surface properties + nullptr, // release proc + nullptr // release context + ); + + if (!result->surface_) { + FML_LOG(ERROR) + << "Could not wrap VkImage as an SkSurface Vulkan render texture."; + return nullptr; + } + + return result; +} + +bool TestVulkanSurface::IsValid() const { + return surface_ != nullptr; +} + +sk_sp TestVulkanSurface::GetSurfaceSnapshot() const { + if (!IsValid()) { + return nullptr; + } + + if (!surface_) { + FML_LOG(ERROR) << "Aborting snapshot because of on-screen surface " + "acquisition failure."; + return nullptr; + } + + auto device_snapshot = surface_->makeImageSnapshot(); + + if (!device_snapshot) { + FML_LOG(ERROR) << "Could not create the device snapshot while attempting " + "to snapshot the Vulkan surface."; + return nullptr; + } + + auto host_snapshot = device_snapshot->makeRasterImage(); + + if (!host_snapshot) { + FML_LOG(ERROR) << "Could not create the host snapshot while attempting to " + "snapshot the Vulkan surface."; + return nullptr; + } + + return host_snapshot; +} + +VkImage TestVulkanSurface::GetImage() { + return image_.GetImage(); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/testing/test_vulkan_surface.h b/engine/src/flutter/testing/test_vulkan_surface.h new file mode 100644 index 00000000000..4dce92fe725 --- /dev/null +++ b/engine/src/flutter/testing/test_vulkan_surface.h @@ -0,0 +1,41 @@ +// 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. + +#ifndef FLUTTER_TESTING_TEST_VULKAN_SURFACE_IMPL_H_ +#define FLUTTER_TESTING_TEST_VULKAN_SURFACE_IMPL_H_ + +#include +#include "flutter/testing/test_vulkan_context.h" + +#include "third_party/skia/include/core/SkRefCnt.h" +#include "third_party/skia/include/core/SkSize.h" +#include "third_party/skia/include/gpu/GrDirectContext.h" + +namespace flutter { + +namespace testing { + +class TestVulkanSurface { + public: + static std::unique_ptr Create( + const TestVulkanContext& context, + const SkISize& surface_size); + + bool IsValid() const; + + sk_sp GetSurfaceSnapshot() const; + + VkImage GetImage(); + + private: + explicit TestVulkanSurface(TestVulkanImage&& image); + + TestVulkanImage image_; + sk_sp surface_; +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_TESTING_TEST_VULKAN_SURFACE_IMPL_H_ diff --git a/engine/src/flutter/vulkan/vulkan_device.cc b/engine/src/flutter/vulkan/vulkan_device.cc index e981329d217..e58dd6e7db3 100644 --- a/engine/src/flutter/vulkan/vulkan_device.cc +++ b/engine/src/flutter/vulkan/vulkan_device.cc @@ -35,8 +35,7 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, : vk(p_vk), physical_device_(std::move(physical_device)), graphics_queue_index_(std::numeric_limits::max()), - valid_(false), - enable_validation_layers_(enable_validation_layers) { + valid_(false) { if (!physical_device_ || !vk.AreInstanceProcsSetup()) { return; } @@ -74,7 +73,7 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, }; auto enabled_layers = - DeviceLayersToEnable(vk, physical_device_, enable_validation_layers_); + DeviceLayersToEnable(vk, physical_device_, enable_validation_layers); const char* layers[enabled_layers.size()]; @@ -122,6 +121,36 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, queue_ = VulkanHandle(queue); + if (!InitializeCommandPool()) { + return; + } + + valid_ = true; +} + +VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, + VulkanHandle physical_device, + VulkanHandle device, + uint32_t queue_family_index, + VulkanHandle queue) + : vk(p_vk), + physical_device_(std::move(physical_device)), + device_(std::move(device)), + queue_(std::move(queue)), + graphics_queue_index_(queue_family_index), + valid_(false) { + if (!physical_device_ || !vk.AreInstanceProcsSetup()) { + return; + } + + if (!InitializeCommandPool()) { + return; + } + + valid_ = true; +} + +bool VulkanDevice::InitializeCommandPool() { const VkCommandPoolCreateInfo command_pool_create_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .pNext = nullptr, @@ -134,7 +163,7 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, nullptr, &command_pool)) != VK_SUCCESS) { FML_DLOG(INFO) << "Could not create the command pool."; - return; + return false; } command_pool_ = VulkanHandle{ @@ -142,7 +171,7 @@ VulkanDevice::VulkanDevice(VulkanProcTable& p_vk, vk.DestroyCommandPool(device_, pool, nullptr); }}; - valid_ = true; + return true; } VulkanDevice::~VulkanDevice() { diff --git a/engine/src/flutter/vulkan/vulkan_device.h b/engine/src/flutter/vulkan/vulkan_device.h index 73219ea098d..22f848dd00e 100644 --- a/engine/src/flutter/vulkan/vulkan_device.h +++ b/engine/src/flutter/vulkan/vulkan_device.h @@ -18,10 +18,20 @@ class VulkanSurface; class VulkanDevice { public: + /// @brief Create a new VkDevice with a resolved VkQueue suitable for + /// rendering with Skia. + /// VulkanDevice(VulkanProcTable& vk, VulkanHandle physical_device, bool enable_validation_layers); + /// @brief Wrap an existing VkDevice and VkQueue. + /// + VulkanDevice(VulkanProcTable& vk, + VulkanHandle physical_device, + VulkanHandle device, + uint32_t queue_family_index, + VulkanHandle queue); ~VulkanDevice(); bool IsValid() const; @@ -72,8 +82,8 @@ class VulkanDevice { VulkanHandle command_pool_; uint32_t graphics_queue_index_; bool valid_; - bool enable_validation_layers_; + bool InitializeCommandPool(); std::vector GetQueueFamilyProperties() const; FML_DISALLOW_COPY_AND_ASSIGN(VulkanDevice); diff --git a/engine/src/flutter/vulkan/vulkan_proc_table.cc b/engine/src/flutter/vulkan/vulkan_proc_table.cc index f5ba8f97bcb..f1ccbe88586 100644 --- a/engine/src/flutter/vulkan/vulkan_proc_table.cc +++ b/engine/src/flutter/vulkan/vulkan_proc_table.cc @@ -16,10 +16,18 @@ namespace vulkan { VulkanProcTable::VulkanProcTable() : VulkanProcTable("libvulkan.so"){}; -VulkanProcTable::VulkanProcTable(const char* path) +VulkanProcTable::VulkanProcTable(const char* so_path) : handle_(nullptr), acquired_mandatory_proc_addresses_(false) { - acquired_mandatory_proc_addresses_ = - OpenLibraryHandle(path) && SetupLoaderProcAddresses(); + acquired_mandatory_proc_addresses_ = OpenLibraryHandle(so_path) && + SetupGetInstanceProcAddress() && + SetupLoaderProcAddresses(); +} + +VulkanProcTable::VulkanProcTable( + std::function get_instance_proc_addr) + : handle_(nullptr), acquired_mandatory_proc_addresses_(false) { + GetInstanceProcAddr = get_instance_proc_addr; + acquired_mandatory_proc_addresses_ = SetupLoaderProcAddresses(); } VulkanProcTable::~VulkanProcTable() { @@ -42,24 +50,27 @@ bool VulkanProcTable::AreDeviceProcsSetup() const { return device_; } -bool VulkanProcTable::SetupLoaderProcAddresses() { +bool VulkanProcTable::SetupGetInstanceProcAddress() { if (!handle_) { return true; } - GetInstanceProcAddr = + GetInstanceProcAddr = reinterpret_cast( #if VULKAN_LINK_STATICALLY - GetInstanceProcAddr = &vkGetInstanceProcAddr; + &vkGetInstanceProcAddr #else // VULKAN_LINK_STATICALLY - reinterpret_cast(const_cast( - handle_->ResolveSymbol("vkGetInstanceProcAddr"))); + const_cast(handle_->ResolveSymbol("vkGetInstanceProcAddr")) #endif // VULKAN_LINK_STATICALLY - + ); if (!GetInstanceProcAddr) { FML_DLOG(WARNING) << "Could not acquire vkGetInstanceProcAddr."; return false; } + return true; +} + +bool VulkanProcTable::SetupLoaderProcAddresses() { VulkanHandle null_instance(VK_NULL_HANDLE, nullptr); ACQUIRE_PROC(CreateInstance, null_instance); @@ -157,7 +168,11 @@ bool VulkanProcTable::OpenLibraryHandle(const char* path) { #else // VULKAN_LINK_STATICALLY handle_ = fml::NativeLibrary::Create(path); #endif // VULKAN_LINK_STATICALLY - return !!handle_; + if (!handle_) { + FML_DLOG(WARNING) << "Could not open Vulkan library handle: " << path; + return false; + } + return true; } bool VulkanProcTable::CloseLibraryHandle() { @@ -173,7 +188,8 @@ PFN_vkVoidFunction VulkanProcTable::AcquireProc( } // A VK_NULL_HANDLE as the instance is an acceptable parameter. - return GetInstanceProcAddr(instance, proc_name); + return reinterpret_cast( + GetInstanceProcAddr(instance, proc_name)); } PFN_vkVoidFunction VulkanProcTable::AcquireProc( diff --git a/engine/src/flutter/vulkan/vulkan_proc_table.h b/engine/src/flutter/vulkan/vulkan_proc_table.h index a7f2a3d5f35..93b4d0a3bfd 100644 --- a/engine/src/flutter/vulkan/vulkan_proc_table.h +++ b/engine/src/flutter/vulkan/vulkan_proc_table.h @@ -48,6 +48,12 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { T proc_; }; + VulkanProcTable(); + explicit VulkanProcTable(const char* so_path); + explicit VulkanProcTable( + std::function get_instance_proc_addr); + ~VulkanProcTable(); + bool HasAcquiredMandatoryProcAddresses() const; bool IsValid() const; @@ -62,6 +68,8 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { GrVkGetProc CreateSkiaGetProc() const; + std::function GetInstanceProcAddr = nullptr; + #define DEFINE_PROC(name) Proc name; DEFINE_PROC(AcquireNextImageKHR); @@ -98,7 +106,6 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { DEFINE_PROC(GetDeviceProcAddr); DEFINE_PROC(GetDeviceQueue); DEFINE_PROC(GetImageMemoryRequirements); - DEFINE_PROC(GetInstanceProcAddr); DEFINE_PROC(GetPhysicalDeviceFeatures); DEFINE_PROC(GetPhysicalDeviceQueueFamilyProperties); DEFINE_PROC(QueueSubmit); @@ -135,10 +142,8 @@ class VulkanProcTable : public fml::RefCountedThreadSafe { VulkanHandle instance_; VulkanHandle device_; - VulkanProcTable(); - explicit VulkanProcTable(const char* path); - ~VulkanProcTable(); bool OpenLibraryHandle(const char* path); + bool SetupGetInstanceProcAddress(); bool SetupLoaderProcAddresses(); bool CloseLibraryHandle(); PFN_vkVoidFunction AcquireProc( diff --git a/engine/src/flutter/vulkan/vulkan_swapchain.cc b/engine/src/flutter/vulkan/vulkan_swapchain.cc index e33fd9742bc..7c3792d3fb5 100644 --- a/engine/src/flutter/vulkan/vulkan_swapchain.cc +++ b/engine/src/flutter/vulkan/vulkan_swapchain.cc @@ -350,7 +350,7 @@ VulkanSwapchain::AcquireResult VulkanSwapchain::AcquireSurface() { // --------------------------------------------------------------------------- // Step 2: - // Put semaphores in unsignaled state. + // Put fences in an unsignaled state. // --------------------------------------------------------------------------- if (!backbuffer->ResetFences()) { FML_DLOG(INFO) << "Could not reset fences."; diff --git a/engine/src/flutter/vulkan/vulkan_window.cc b/engine/src/flutter/vulkan/vulkan_window.cc index 1cee0820c61..cf7730c7aa7 100644 --- a/engine/src/flutter/vulkan/vulkan_window.cc +++ b/engine/src/flutter/vulkan/vulkan_window.cc @@ -19,24 +19,21 @@ namespace vulkan { VulkanWindow::VulkanWindow(fml::RefPtr proc_table, - std::unique_ptr native_surface, - bool render_to_surface) + std::unique_ptr native_surface) : VulkanWindow(/*context/*/ nullptr, proc_table, - std::move(native_surface), - render_to_surface) {} + std::move(native_surface)) {} VulkanWindow::VulkanWindow(const sk_sp& context, fml::RefPtr proc_table, - std::unique_ptr native_surface, - bool render_to_surface) + std::unique_ptr native_surface) : valid_(false), vk(std::move(proc_table)), skia_gr_context_(context) { if (!vk || !vk->HasAcquiredMandatoryProcAddresses()) { FML_DLOG(INFO) << "Proc table has not acquired mandatory proc addresses."; return; } - if (native_surface == nullptr || !native_surface->IsValid()) { + if (native_surface && !native_surface->IsValid()) { FML_DLOG(INFO) << "Native surface is invalid."; return; } @@ -70,9 +67,7 @@ VulkanWindow::VulkanWindow(const sk_sp& context, return; } - // TODO(38466): Refactor GPU surface APIs take into account the fact that an - // external view embedder may want to render to the root surface. - if (!render_to_surface) { + if (!native_surface) { return; } diff --git a/engine/src/flutter/vulkan/vulkan_window.h b/engine/src/flutter/vulkan/vulkan_window.h index 685825024f7..bbaba38d47e 100644 --- a/engine/src/flutter/vulkan/vulkan_window.h +++ b/engine/src/flutter/vulkan/vulkan_window.h @@ -36,8 +36,7 @@ class VulkanWindow { /// GrDirectContext. /// VulkanWindow(fml::RefPtr proc_table, - std::unique_ptr native_surface, - bool render_to_surface); + std::unique_ptr native_surface); //------------------------------------------------------------------------------ /// @brief Construct a VulkanWindow. Let reuse an existing @@ -45,8 +44,7 @@ class VulkanWindow { /// VulkanWindow(const sk_sp& context, fml::RefPtr proc_table, - std::unique_ptr native_surface, - bool render_to_surface); + std::unique_ptr native_surface); ~VulkanWindow();