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 451829e086..21443efbc6 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 9687b94fbb..ec1d377d96 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 56112042fb..cb3900e392 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 570783bffa..6040f459cc 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 50c420e387..b281ac8c0f 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 9b1a495959..023fe92619 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 622432d2c1..1e6ac775d5 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 838c4c4965..0f826a5701 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 e8b769ef4c..a8a2592c62 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 0000000000..b3918a2420 --- /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 0000000000..3fa28fa09a --- /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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 C{t?Fj 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 2a2752446f..a24e13e263 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 22f4d2dee6..5c40ce1e16 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 423c1239f3..e631305852 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 d04188e0d3..ae63a3220f 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 ee1fb514bb..73256a36b3 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 b816ab5d22..9a315dec3c 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 f00d802e0f..eb16637872 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 6d0c6b9d25..94fa852d10 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 f993891e44..956c565d29 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 0000000000..a932260513 --- /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 0000000000..608bda52ec --- /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 3f4f6d06b9..f7df168122 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 0000000000..0fb93fa1e3 --- /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 0000000000..b08aa43941 --- /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 c8086191c8..94f2eddb42 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 89d5fec995..d076dc1c01 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 35944cb13a..f02c1d61c0 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 9ae50f47e8..3b47f5ac25 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 36c30989d0..56eeb558cc 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 5d79971809..25d212bf05 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 0000000000..4c365412ae --- /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 0000000000..c8a6a797f3 --- /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 0000000000..6bca8ed44f --- /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 0000000000..4dce92fe72 --- /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 e981329d21..e58dd6e7db 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 73219ea098..22f848dd00 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 f5ba8f97bc..f1ccbe8858 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 a7f2a3d5f3..93b4d0a3bf 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 e33fd9742b..7c3792d3fb 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 1cee0820c6..cf7730c7aa 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 685825024f..bbaba38d47 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();