From 3fb8de3035bb739792ace9312a847129ad714400 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 28 Sep 2023 16:02:26 -0700 Subject: [PATCH 01/22] [Impeller] flutter_tester --enable-impeller --- impeller/aiks/canvas.cc | 2 + impeller/base/validation.cc | 2 +- .../renderer/backend/vulkan/allocator_vk.cc | 1 + .../backend/vulkan/surface_context_vk.cc | 4 + .../backend/vulkan/swapchain_impl_vk.cc | 1 + .../renderer/backend/vulkan/swapchain_vk.cc | 2 + impeller/tools/impeller.gni | 4 +- lib/gpu/context.cc | 3 +- ..._2_dispose_op_restore_previous.apng.67.png | Bin 0 -> 23284 bytes ..._2_dispose_op_restore_previous.apng.68.png | Bin 0 -> 23229 bytes ..._2_dispose_op_restore_previous.apng.69.png | Bin 0 -> 23438 bytes .../impeller_four_frame_with_reuse_end.png | Bin 0 -> 506 bytes lib/ui/fixtures/impeller_heart_end.png | Bin 0 -> 39702 bytes shell/common/shell.cc | 5 + shell/gpu/gpu_surface_vulkan.cc | 4 + shell/gpu/gpu_surface_vulkan_impeller.cc | 5 + shell/testing/BUILD.gn | 29 +++++ shell/testing/tester_main.cc | 112 +++++++++++++++++- testing/dart/canvas_test.dart | 10 +- testing/dart/codec_test.dart | 19 ++- testing/dart/color_filter_test.dart | 22 ++++ testing/dart/encoding_test.dart | 14 +++ testing/dart/fragment_shader_test.dart | 47 ++++++++ testing/dart/gpu_test.dart | 8 ++ testing/dart/image_filter_test.dart | 22 ++++ testing/dart/observatory/skp_test.dart | 21 +++- testing/dart/observatory/tracing_test.dart | 8 +- .../observatory/vmservice_methods_test.dart | 5 +- .../impeller_canvas_test_toImage.png | Bin 0 -> 427 bytes vulkan/vulkan_window.cc | 4 + 30 files changed, 331 insertions(+), 23 deletions(-) create mode 100644 lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.67.png create mode 100644 lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.68.png create mode 100644 lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.69.png create mode 100644 lib/ui/fixtures/impeller_four_frame_with_reuse_end.png create mode 100644 lib/ui/fixtures/impeller_heart_end.png create mode 100644 testing/resources/impeller_canvas_test_toImage.png diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index ae4c9d512037f..b14e95177e0b8 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -9,6 +9,7 @@ #include #include "flutter/fml/logging.h" +#include "flutter/fml/trace_event.h" #include "impeller/aiks/image_filter.h" #include "impeller/aiks/paint_pass_delegate.h" #include "impeller/entity/contents/atlas_contents.h" @@ -532,6 +533,7 @@ size_t Canvas::GetStencilDepth() const { void Canvas::SaveLayer(const Paint& paint, std::optional bounds, const std::shared_ptr& backdrop_filter) { + TRACE_EVENT0("flutter", "Canvas::saveLayer"); Save(true, paint.blend_mode, backdrop_filter); auto& new_layer_pass = GetCurrentPass(); diff --git a/impeller/base/validation.cc b/impeller/base/validation.cc index bb41c50fec462..bbfd7efda3710 100644 --- a/impeller/base/validation.cc +++ b/impeller/base/validation.cc @@ -48,7 +48,7 @@ void ImpellerValidationBreak(const char* message) { } else { FML_LOG(ERROR) << stream.str(); } -#endif // IMPELLER_DEBUG +#endif // IMPELLER_ENABLE_VALIDATION } } // namespace impeller diff --git a/impeller/renderer/backend/vulkan/allocator_vk.cc b/impeller/renderer/backend/vulkan/allocator_vk.cc index 3fc4f2f99ca5a..f90ee4bc447d6 100644 --- a/impeller/renderer/backend/vulkan/allocator_vk.cc +++ b/impeller/renderer/backend/vulkan/allocator_vk.cc @@ -265,6 +265,7 @@ class AllocatedTextureSourceVK final : public TextureSourceVK { vk::Device device, bool supports_memoryless_textures) : TextureSourceVK(desc), resource_(std::move(resource_manager)) { + FML_DCHECK(desc.format != PixelFormat::kUnknown); TRACE_EVENT0("impeller", "CreateDeviceTexture"); vk::ImageCreateInfo image_info; image_info.flags = ToVKImageCreateFlags(desc.type); diff --git a/impeller/renderer/backend/vulkan/surface_context_vk.cc b/impeller/renderer/backend/vulkan/surface_context_vk.cc index 2e20d2af5cbe6..afc1407f6f3f5 100644 --- a/impeller/renderer/backend/vulkan/surface_context_vk.cc +++ b/impeller/renderer/backend/vulkan/surface_context_vk.cc @@ -63,6 +63,10 @@ bool SurfaceContextVK::SetWindowSurface(vk::UniqueSurfaceKHR surface) { VALIDATION_LOG << "Could not create swapchain."; return false; } + if (!swapchain->IsValid()) { + VALIDATION_LOG << "Could not create valid swapchain."; + return false; + } swapchain_ = std::move(swapchain); return true; } diff --git a/impeller/renderer/backend/vulkan/swapchain_impl_vk.cc b/impeller/renderer/backend/vulkan/swapchain_impl_vk.cc index 0af88001b8e95..dcd5d1c50b593 100644 --- a/impeller/renderer/backend/vulkan/swapchain_impl_vk.cc +++ b/impeller/renderer/backend/vulkan/swapchain_impl_vk.cc @@ -142,6 +142,7 @@ SwapchainImplVK::SwapchainImplVK( vk::SwapchainKHR old_swapchain, vk::SurfaceTransformFlagBitsKHR last_transform) { if (!context) { + VALIDATION_LOG << "Cannot create a swapchain without a context."; return; } diff --git a/impeller/renderer/backend/vulkan/swapchain_vk.cc b/impeller/renderer/backend/vulkan/swapchain_vk.cc index 0a09a9eb371e0..c5399ce4835e7 100644 --- a/impeller/renderer/backend/vulkan/swapchain_vk.cc +++ b/impeller/renderer/backend/vulkan/swapchain_vk.cc @@ -5,6 +5,7 @@ #include "impeller/renderer/backend/vulkan/swapchain_vk.h" #include "flutter/fml/trace_event.h" +#include "impeller/base/validation.h" #include "impeller/renderer/backend/vulkan/swapchain_impl_vk.h" namespace impeller { @@ -14,6 +15,7 @@ std::shared_ptr SwapchainVK::Create( vk::UniqueSurfaceKHR surface) { auto impl = SwapchainImplVK::Create(context, std::move(surface)); if (!impl || !impl->IsValid()) { + VALIDATION_LOG << "Failed to create SwapchainVK implementation."; return nullptr; } return std::shared_ptr(new SwapchainVK(std::move(impl))); diff --git a/impeller/tools/impeller.gni b/impeller/tools/impeller.gni index f2d33b0c28e4a..e21ef0ef8ef3a 100644 --- a/impeller/tools/impeller.gni +++ b/impeller/tools/impeller.gni @@ -22,8 +22,8 @@ declare_args() { (is_linux || is_win || is_android) && target_os != "fuchsia" # Whether the Vulkan backend is enabled. - impeller_enable_vulkan = - (is_linux || is_win || is_android) && target_os != "fuchsia" + impeller_enable_vulkan = (is_linux || is_win || is_android || + enable_unittests) && target_os != "fuchsia" # Whether to use a prebuilt impellerc. # If this is the empty string, impellerc will be built. diff --git a/lib/gpu/context.cc b/lib/gpu/context.cc index 34418320b5ddc..7539f0cb7300a 100644 --- a/lib/gpu/context.cc +++ b/lib/gpu/context.cc @@ -55,7 +55,8 @@ Dart_Handle InternalFlutterGpu_Context_InitializeDefault(Dart_Handle wrapper) { // Grab the Impeller context from the IO manager. std::promise> context_promise; auto impeller_context_future = context_promise.get_future(); - dart_state->GetTaskRunners().GetIOTaskRunner()->PostTask( + fml::TaskRunner::RunNowOrPostTask( + dart_state->GetTaskRunners().GetIOTaskRunner(), fml::MakeCopyable([promise = std::move(context_promise), io_manager = dart_state->GetIOManager()]() mutable { promise.set_value(io_manager ? io_manager->GetImpellerContext() diff --git a/lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.67.png b/lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.67.png new file mode 100644 index 0000000000000000000000000000000000000000..bdec9bc8f149955ba3eaea280d583fef4ec368d7 GIT binary patch literal 23284 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%0DX>Hx<$x%cNt9! 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 literal 0 HcmV?d00001 diff --git a/lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.69.png b/lib/ui/fixtures/impeller_2_dispose_op_restore_previous.apng.69.png new file mode 100644 index 0000000000000000000000000000000000000000..c3006d0c2f89b85cc07349026bae3ebf9beb370f GIT binary patch literal 23438 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

?UnuQ1G&L%ATcb7caNHV7^@ZCCVIYa7sw(L@ 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 literal 0 HcmV?d00001 diff --git a/lib/ui/fixtures/impeller_heart_end.png b/lib/ui/fixtures/impeller_heart_end.png new file mode 100644 index 0000000000000000000000000000000000000000..cd33ff064c60571b28d14aff5bb3235993e7eca9 GIT binary patch literal 39702 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 + if (settings_.enable_impeller) { + ServiceProtocolFailureError( + response, "Cannot capture SKP screenshot with Impeller enabled."); + return false; + } auto screenshot = rasterizer_->ScreenshotLastLayerTree( Rasterizer::ScreenshotType::SkiaPicture, true); if (screenshot.data) { diff --git a/shell/gpu/gpu_surface_vulkan.cc b/shell/gpu/gpu_surface_vulkan.cc index a8976d677afd0..63f1359722c17 100644 --- a/shell/gpu/gpu_surface_vulkan.cc +++ b/shell/gpu/gpu_surface_vulkan.cc @@ -104,6 +104,7 @@ sk_sp GPUSurfaceVulkan::CreateSurfaceFromVulkanImage( const VkImage image, const VkFormat format, const SkISize& size) { +#ifdef SK_VULKAN GrVkImageInfo image_info = { .fImage = image, .fImageTiling = VK_IMAGE_TILING_OPTIMAL, @@ -130,6 +131,9 @@ sk_sp GPUSurfaceVulkan::CreateSurfaceFromVulkanImage( SkColorSpace::MakeSRGB(), // color space &surface_properties // surface properties ); +#else + return nullptr; +#endif // SK_VULKAN } SkColorType GPUSurfaceVulkan::ColorTypeFromFormat(const VkFormat format) { diff --git a/shell/gpu/gpu_surface_vulkan_impeller.cc b/shell/gpu/gpu_surface_vulkan_impeller.cc index 3b6ec9d876ec0..917b5f0e6e11d 100644 --- a/shell/gpu/gpu_surface_vulkan_impeller.cc +++ b/shell/gpu/gpu_surface_vulkan_impeller.cc @@ -60,6 +60,11 @@ std::unique_ptr GPUSurfaceVulkanImpeller::AcquireFrame( auto& context_vk = impeller::SurfaceContextVK::Cast(*impeller_context_); std::unique_ptr surface = context_vk.AcquireNextSurface(); + if (!surface) { + FML_LOG(ERROR) << "No surface available."; + return nullptr; + } + SurfaceFrame::SubmitCallback submit_callback = fml::MakeCopyable([renderer = impeller_renderer_, // aiks_context = aiks_context_, // diff --git a/shell/testing/BUILD.gn b/shell/testing/BUILD.gn index ec6400ce0763a..9903a9356b61f 100644 --- a/shell/testing/BUILD.gn +++ b/shell/testing/BUILD.gn @@ -3,6 +3,15 @@ # found in the LICENSE file. import("//build/fuchsia/sdk.gni") +import("//flutter/impeller/tools/impeller.gni") +import("//flutter/shell/gpu/gpu.gni") + +shell_gpu_configuration("tester_gpu_configuration") { + enable_software = true + enable_gl = true + enable_vulkan = true + enable_metal = false +} executable("testing") { output_name = "flutter_tester" @@ -36,6 +45,26 @@ executable("testing") { "//third_party/skia", ] + if (impeller_supports_rendering) { + deps += [ + ":tester_gpu_configuration", + "//flutter/impeller", + "//third_party/swiftshader", + ] + + if (is_win) { + libs = [ "$root_out_dir/vk_swiftshader.dll" ] + } else if (is_mac) { + libs = [ "$root_out_dir/libvk_swiftshader.dylib" ] + } else if (is_linux) { + libs = [ "$root_out_dir/libvk_swiftshader.so" ] + } else { + assert( + false, + "Only Windows, Mac, and Linux are supported for this target with Impeller.") + } + } + metadata = { entitlement_file_path = [ "flutter_tester" ] } diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 310ce62e01430..c4ac7dfb8902f 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "impeller/renderer/surface.h" #define FML_USED_ON_EMBEDDER #include @@ -29,6 +30,44 @@ #include "third_party/dart/runtime/include/dart_api.h" #include "third_party/skia/include/core/SkSurface.h" +#if IMPELLER_SUPPORTS_RENDERING +#include // nogncheck +#include "impeller/core/formats.h" // nogncheck +#include "impeller/entity/vk/entity_shaders_vk.h" // nogncheck +#include "impeller/entity/vk/modern_shaders_vk.h" // nogncheck +#include "impeller/renderer/backend/vulkan/context_vk.h" // nogncheck +#include "impeller/renderer/backend/vulkan/surface_context_vk.h" // nogncheck +#include "impeller/renderer/context.h" // nogncheck +#include "impeller/renderer/vk/compute_shaders_vk.h" // nogncheck +#include "shell/gpu/gpu_surface_vulkan_impeller.h" // nogncheck +#if IMPELLER_ENABLE_3D +#include "impeller/scene/shaders/vk/scene_shaders_vk.h" // nogncheck +#endif // IMPELLER_ENABLE_3D + +static std::vector> ShaderLibraryMappings() { + return { + std::make_shared(impeller_entity_shaders_vk_data, + impeller_entity_shaders_vk_length), + std::make_shared( + impeller_modern_shaders_vk_data, impeller_modern_shaders_vk_length), +#if IMPELLER_ENABLE_3D + std::make_shared( + impeller_scene_shaders_vk_data, impeller_scene_shaders_vk_length), +#endif // IMPELLER_ENABLE_3D + std::make_shared( + impeller_compute_shaders_vk_data, + impeller_compute_shaders_vk_length), + }; +} + +#else +namespace impeller { +class Context; +class ContextVK; +class SurfaceContextVK; +} // namespace impeller +#endif // IMPELLER_SUPPORTS_RENDERING + #if defined(FML_OS_WIN) #include #endif // defined(FML_OS_WIN) @@ -81,11 +120,30 @@ class TesterGPUSurfaceSoftware : public GPUSurfaceSoftware { class TesterPlatformView : public PlatformView, public GPUSurfaceSoftwareDelegate { public: - TesterPlatformView(Delegate& delegate, const TaskRunners& task_runners) - : PlatformView(delegate, task_runners) {} + TesterPlatformView(Delegate& delegate, + const TaskRunners& task_runners, + const std::shared_ptr& impeller_context, + const std::shared_ptr& + impeller_surface_context) + : PlatformView(delegate, task_runners), + impeller_context_(impeller_context), + impeller_surface_context_(impeller_surface_context) {} + + // |PlatformView| + std::shared_ptr GetImpellerContext() const override { + return impeller_context_; + } // |PlatformView| std::unique_ptr CreateRenderingSurface() override { + if (delegate_.OnPlatformViewGetSettings().enable_impeller) { + FML_DCHECK(impeller_context_); + auto surface = + std::make_unique(impeller_surface_context_); + FML_DCHECK(surface->IsValid()); + return surface; + } + FML_DCHECK(!impeller_context_); auto surface = std::make_unique( this, true /* render to surface */); FML_DCHECK(surface->IsValid()); @@ -126,6 +184,8 @@ class TesterPlatformView : public PlatformView, private: sk_sp sk_surface_ = nullptr; + std::shared_ptr impeller_context_; + std::shared_ptr impeller_surface_context_; std::shared_ptr external_view_embedder_ = std::make_shared(); }; @@ -235,10 +295,52 @@ int RunTester(const flutter::Settings& settings, io_task_runner // io ); + std::shared_ptr impeller_context; + std::shared_ptr impeller_surface_context; + +#if IMPELLER_SUPPORTS_RENDERING + if (settings.enable_impeller) { + impeller::ContextVK::Settings context_settings; + context_settings.proc_address_callback = + reinterpret_cast(&::vkGetInstanceProcAddr); + context_settings.shader_libraries_data = ShaderLibraryMappings(); + context_settings.cache_directory = fml::paths::GetCachesDirectory(); + context_settings.enable_validation = settings.enable_vulkan_validation; + + impeller_context = impeller::ContextVK::Create(std::move(context_settings)); + if (!impeller_context || !impeller_context->IsValid()) { + VALIDATION_LOG << "Could not create Vulkan context."; + return -1; + } + + impeller::vk::SurfaceKHR vk_surface; + impeller::vk::HeadlessSurfaceCreateInfoEXT surface_create_info; + auto res = impeller_context->GetInstance().createHeadlessSurfaceEXT( + &surface_create_info, // surface create info + nullptr, // allocator + &vk_surface // surface + ); + if (res != impeller::vk::Result::eSuccess) { + VALIDATION_LOG << "Could not create surface for tester " + << impeller::vk::to_string(res); + return -1; + } + + impeller::vk::UniqueSurfaceKHR surface{vk_surface, + impeller_context->GetInstance()}; + impeller_surface_context = impeller_context->CreateSurfaceContext(); + if (!impeller_surface_context->SetWindowSurface(std::move(surface))) { + VALIDATION_LOG << "Could not set up surface for context."; + return -1; + } + } +#endif // IMPELLER_SUPPORTS_RENDERING + Shell::CreateCallback on_create_platform_view = - [](Shell& shell) { - return std::make_unique(shell, - shell.GetTaskRunners()); + [impeller_context, impeller_surface_context](Shell& shell) { + return std::make_unique( + shell, shell.GetTaskRunners(), impeller_context, + impeller_surface_context); }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 716233fa8a783..c0aba82e63c3a 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -14,6 +14,8 @@ import 'package:vector_math/vector_math_64.dart'; typedef CanvasCallback = void Function(Canvas canvas); +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + Future createImage(int width, int height) { final Completer completer = Completer(); decodeImageFromPixels( @@ -190,7 +192,7 @@ void main() { expect(image.height, equals(100)); final bool areEqual = - await fuzzyGoldenImageCompare(image, 'canvas_test_toImage.png'); + await fuzzyGoldenImageCompare(image, '${impellerEnabled ? 'impeller_' : ''}canvas_test_toImage.png'); expect(areEqual, true); }); @@ -438,6 +440,12 @@ void main() { recorder = PictureRecorder(); canvas = Canvas(recorder); + if (impellerEnabled) { + // Impeller tries to automagically scale this. See + // https://github.com/flutter/flutter/issues/128885 + canvas.drawImage(image, Offset.zero, Paint()); + return; + } // On a slower CI machine, the raster thread may get behind the UI thread // here. However, once the image is in an error state it will immediately // throw on subsequent attempts. diff --git a/testing/dart/codec_test.dart b/testing/dart/codec_test.dart index fb07a63f61a21..365e1ddeffb49 100644 --- a/testing/dart/codec_test.dart +++ b/testing/dart/codec_test.dart @@ -9,6 +9,8 @@ import 'dart:ui' as ui; import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + void main() { test('Animation metadata', () async { @@ -43,7 +45,11 @@ void main() { await codec.getNextFrame(); fail('exception not thrown'); } on Exception catch (e) { - expect(e.toString(), contains('Codec failed')); + if (impellerEnabled) { + expect(e.toString(), contains('Could not decompress image.')); + } else { + expect(e.toString(), contains('Codec failed')); + } } }); @@ -145,8 +151,9 @@ void main() { final ui.Image image = frameInfo.image; final ByteData imageData = (await image.toByteData(format: ui.ImageByteFormat.png))!; + final String fileName = impellerEnabled ? 'impeller_four_frame_with_reuse_end.png' : 'four_frame_with_reuse_end.png'; final Uint8List goldenData = File( - path.join('flutter', 'lib', 'ui', 'fixtures', 'four_frame_with_reuse_end.png'), + path.join('flutter', 'lib', 'ui', 'fixtures', fileName), ).readAsBytesSync(); expect(imageData.buffer.asUint8List(), goldenData); @@ -170,8 +177,10 @@ void main() { final ui.Image image = frameInfo.image; final ByteData imageData = (await image.toByteData(format: ui.ImageByteFormat.png))!; + final String fileName = impellerEnabled ? 'impeller_heart_end.png' : 'heart_end.png'; + final Uint8List goldenData = File( - path.join('flutter', 'lib', 'ui', 'fixtures', 'heart_end.png'), + path.join('flutter', 'lib', 'ui', 'fixtures', fileName), ).readAsBytesSync(); expect(imageData.buffer.asUint8List(), goldenData); @@ -194,8 +203,10 @@ void main() { final ui.Image image = frameInfo.image; final ByteData imageData = (await image.toByteData(format: ui.ImageByteFormat.png))!; + final String fileName = impellerEnabled ? 'impeller_2_dispose_op_restore_previous.apng.$i.png' : '2_dispose_op_restore_previous.apng.$i.png'; + final Uint8List goldenData = File( - path.join('flutter', 'lib', 'ui', 'fixtures', '2_dispose_op_restore_previous.apng.$i.png'), + path.join('flutter', 'lib', 'ui', 'fixtures', fileName), ).readAsBytesSync(); expect(imageData.buffer.asUint8List(), goldenData); diff --git a/testing/dart/color_filter_test.dart b/testing/dart/color_filter_test.dart index e5553604da9b2..41a8d86067338 100644 --- a/testing/dart/color_filter_test.dart +++ b/testing/dart/color_filter_test.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + const Color transparent = Color(0x00000000); const Color red = Color(0xFFAA0000); const Color green = Color(0xFF00AA00); @@ -56,6 +59,11 @@ void main() { Uint32List bytes = await getBytesForPaint(paint); expect(bytes[0], greenRedColorBlend); + // TODO(135699): enable this + if (impellerEnabled) { + return; + } + paint.invertColors = true; bytes = await getBytesForPaint(paint); expect(bytes[0], greenRedColorBlendInverted); @@ -87,6 +95,11 @@ void main() { Uint32List bytes = await getBytesForPaint(paint); expect(bytes[0], greenGreyscaled); + // TODO(135699): enable this + if (impellerEnabled) { + return; + } + paint.invertColors = true; bytes = await getBytesForPaint(paint); expect(bytes[0], greenInvertedGreyscaled); @@ -118,6 +131,10 @@ void main() { Uint32List bytes = await getBytesForPaint(paint); expect(bytes[0], greenLinearToSrgbGamma); + // TODO(135699): enable this + if (impellerEnabled) { + return; + } paint.invertColors = true; bytes = await getBytesForPaint(paint); expect(bytes[0], greenLinearToSrgbGammaInverted); @@ -131,6 +148,11 @@ void main() { Uint32List bytes = await getBytesForPaint(paint); expect(bytes[0], greenSrgbToLinearGamma); + // TODO(135699): enable this + if (impellerEnabled) { + return; + } + paint.invertColors = true; bytes = await getBytesForPaint(paint); expect(bytes[0], greenSrgbToLinearGammaInverted); diff --git a/testing/dart/encoding_test.dart b/testing/dart/encoding_test.dart index 9ca46b9306d02..5b0ce19834368 100644 --- a/testing/dart/encoding_test.dart +++ b/testing/dart/encoding_test.dart @@ -10,6 +10,8 @@ import 'dart:ui'; import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + const int _kWidth = 10; const int _kRadius = 2; @@ -18,6 +20,10 @@ const Color _kGreen = Color.fromRGBO(0, 255, 0, 1.0); void main() { test('decodeImageFromPixels float32', () async { + if (impellerEnabled) { + print('Disabled on Impeller - https://github.com/flutter/flutter/issues/135702'); + return; + } const int width = 2; const int height = 2; final Float32List pixels = Float32List(width * height * 4); @@ -91,6 +97,10 @@ void main() { }); test('Image.toByteData Unmodified format works with grayscale images', () async { + if (impellerEnabled) { + print('Disabled on Impeller - https://github.com/flutter/flutter/issues/135706'); + return; + } final Image image = await GrayscaleImage.load(); final ByteData data = (await image.toByteData(format: ImageByteFormat.rawUnmodified))!; final Uint8List bytes = data.buffer.asUint8List(); @@ -99,6 +109,10 @@ void main() { }); test('Image.toByteData PNG format works with simple image', () async { + if (impellerEnabled) { + print('Disabled on Impeller - https://github.com/flutter/flutter/issues/135706'); + return; + } final Image image = await Square4x4Image.image; final ByteData data = (await image.toByteData(format: ImageByteFormat.png))!; final List expected = await readFile('square.png'); diff --git a/testing/dart/fragment_shader_test.dart b/testing/dart/fragment_shader_test.dart index e97eb5128f6e5..56837c89aa04e 100644 --- a/testing/dart/fragment_shader_test.dart +++ b/testing/dart/fragment_shader_test.dart @@ -14,6 +14,8 @@ import 'package:path/path.dart' as path; import 'shader_test_file_utils.dart'; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + void main() async { bool assertsEnabled = false; assert(() { @@ -172,6 +174,10 @@ void main() async { }); test('FragmentShader simple shader renders correctly', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'functions.frag.iplr', ); @@ -182,6 +188,10 @@ void main() async { }); test('Reused FragmentShader simple shader renders correctly', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'functions.frag.iplr', ); @@ -196,6 +206,10 @@ void main() async { }); test('FragmentShader blue-green image renders green', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'blue_green_sampler.frag.iplr', ); @@ -208,6 +222,10 @@ void main() async { }); test('FragmentShader blue-green image renders green - GPU image', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'blue_green_sampler.frag.iplr', ); @@ -220,6 +238,10 @@ void main() async { }); test('FragmentShader with uniforms renders correctly', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'uniforms.frag.iplr', ); @@ -246,6 +268,10 @@ void main() async { }); test('FragmentShader shader with array uniforms renders correctly', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'uniform_arrays.frag.iplr', ); @@ -260,6 +286,10 @@ void main() async { }); test('FragmentShader The ink_sparkle shader is accepted', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'ink_sparkle.frag.iplr', ); @@ -273,6 +303,10 @@ void main() async { }); test('FragmentShader Uniforms are sorted correctly', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'uniforms_sorted.frag.iplr', ); @@ -314,6 +348,10 @@ void main() async { }); test('FragmentShader user defined functions do not redefine builtins', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'no_builtin_redefinition.frag.iplr', ); @@ -324,6 +362,10 @@ void main() async { }); test('FragmentShader fromAsset accepts a shader with no uniforms', () async { + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } final FragmentProgram program = await FragmentProgram.fromAsset( 'no_uniforms.frag.iplr', ); @@ -332,6 +374,11 @@ void main() async { shader.dispose(); }); + if (impellerEnabled) { + print('Skipped for Impeller - https://github.com/flutter/flutter/issues/122823'); + return; + } + // Test all supported GLSL ops. See lib/spirv/lib/src/constants.dart final Map iplrSupportedGLSLOpShaders = await _loadShaderAssets( path.join('supported_glsl_op_shaders', 'iplr'), diff --git a/testing/dart/gpu_test.dart b/testing/dart/gpu_test.dart index 0c96b6646dc0b..d4925e6b54d3a 100644 --- a/testing/dart/gpu_test.dart +++ b/testing/dart/gpu_test.dart @@ -4,9 +4,13 @@ // ignore_for_file: avoid_relative_lib_imports +import 'dart:io'; + import 'package:litetest/litetest.dart'; import '../../lib/gpu/lib/gpu.dart' as gpu; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + void main() { // TODO(131346): Remove this once we migrate the Dart GPU API into this space. test('smoketest', () async { @@ -23,6 +27,10 @@ void main() { }); test('gpu.context throws exception for incompatible embedders', () async { + if (impellerEnabled) { + expect(gpu.gpuContext != null, true); + return; + } try { // ignore: unnecessary_statements gpu.gpuContext; // Force the diff --git a/testing/dart/image_filter_test.dart b/testing/dart/image_filter_test.dart index 0fa8651656300..9142362845ebc 100644 --- a/testing/dart/image_filter_test.dart +++ b/testing/dart/image_filter_test.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + const Color red = Color(0xFFAA0000); const Color green = Color(0xFF00AA00); @@ -174,6 +177,10 @@ void main() { } test('ImageFilter - blur', () async { + if (impellerEnabled) { + print('Disabled - see https://github.com/flutter/flutter/issues/135712'); + return; + } final Paint paint = Paint() ..color = green ..imageFilter = makeBlur(1.0, 1.0, TileMode.decal); @@ -201,6 +208,11 @@ void main() { }); test('ImageFilter - matrix', () async { + if (impellerEnabled) { + print('Disabled - see https://github.com/flutter/flutter/issues/135712'); + return; + } + final Paint paint = Paint() ..color = green ..imageFilter = makeScale(2.0, 2.0, 1.5, 1.5); @@ -227,6 +239,11 @@ void main() { }); test('ImageFilter - from color filters', () async { + if (impellerEnabled) { + print('Disabled - see https://github.com/flutter/flutter/issues/135712'); + return; + } + final Paint paint = Paint() ..color = green ..imageFilter = const ColorFilter.matrix(constValueColorMatrix); @@ -236,6 +253,11 @@ void main() { }); test('ImageFilter - color filter composition', () async { + if (impellerEnabled) { + print('Disabled - see https://github.com/flutter/flutter/issues/135712'); + return; + } + final ImageFilter compOrder1 = ImageFilter.compose( outer: const ColorFilter.matrix(halvesBrightnessColorMatrix), inner: const ColorFilter.matrix(constValueColorMatrix), diff --git a/testing/dart/observatory/skp_test.dart b/testing/dart/observatory/skp_test.dart index 239e3b529f27a..2414f39faee3c 100644 --- a/testing/dart/observatory/skp_test.dart +++ b/testing/dart/observatory/skp_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; @@ -12,6 +13,8 @@ import 'package:litetest/litetest.dart'; import 'package:vm_service/vm_service.dart' as vms; import 'package:vm_service/vm_service_io.dart'; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + void main() { test('Capture an SKP ', () async { final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); @@ -42,13 +45,19 @@ void main() { PlatformDispatcher.instance.scheduleFrame(); await completer.future; - final vms.Response response = await vmService.callServiceExtension('_flutter.screenshotSkp'); + try { + final vms.Response response = await vmService.callServiceExtension('_flutter.screenshotSkp'); + expect(impellerEnabled, false); + final String base64data = response.json!['skp'] as String; + expect(base64data, isNotNull); + expect(base64data, isNotEmpty); + final Uint8List decoded = base64Decode(base64data); + expect(decoded.sublist(0, 8), 'skiapict'.codeUnits); + } on vms.RPCError catch (e) { + expect(impellerEnabled, true); + expect(e.toString(), contains('Cannot capture SKP screenshot with Impeller enabled.')); + } - final String base64data = response.json!['skp'] as String; - expect(base64data, isNotNull); - expect(base64data, isNotEmpty); - final Uint8List decoded = base64Decode(base64data); - expect(decoded.sublist(0, 8), 'skiapict'.codeUnits); await vmService.dispose(); }); diff --git a/testing/dart/observatory/tracing_test.dart b/testing/dart/observatory/tracing_test.dart index 1937c9b11df1e..ea0674934a1fe 100644 --- a/testing/dart/observatory/tracing_test.dart +++ b/testing/dart/observatory/tracing_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert' show base64Decode; +import 'dart:io'; import 'dart:developer' as developer; import 'dart:ui'; @@ -12,6 +13,8 @@ import 'package:vm_service/vm_service.dart' as vms; import 'package:vm_service/vm_service_io.dart'; import 'package:vm_service_protos/vm_service_protos.dart'; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + Future _testChromeFormatTrace(vms.VmService vmService) async { final vms.Timeline timeline = await vmService.getVMTimeline(); @@ -32,7 +35,7 @@ Future _testChromeFormatTrace(vms.VmService vmService) async { } } expect(saveLayerRecordCount, 3); - expect(saveLayerCount, 3); + expect(saveLayerCount, impellerEnabled ? 2 : 3); expect(flowEventCount, 5); } @@ -59,7 +62,7 @@ Future _testPerfettoFormatTrace(vms.VmService vmService) async { } } expect(saveLayerRecordCount, 3); - expect(saveLayerCount, 3); + expect(saveLayerCount, impellerEnabled ? 2 : 3); expect(flowIdCount, 5); } @@ -80,6 +83,7 @@ void main() { final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); canvas.drawColor(const Color(0xff0000ff), BlendMode.srcOut); + // Will saveLayer implicitly for Skia, but not Impeller. canvas.drawPaint(Paint()..imageFilter = ImageFilter.blur(sigmaX: 2, sigmaY: 3)); canvas.saveLayer(null, Paint()); canvas.drawRect(const Rect.fromLTRB(10, 10, 20, 20), Paint()); diff --git a/testing/dart/observatory/vmservice_methods_test.dart b/testing/dart/observatory/vmservice_methods_test.dart index fa522b751ed09..cd9c4ec93b15c 100644 --- a/testing/dart/observatory/vmservice_methods_test.dart +++ b/testing/dart/observatory/vmservice_methods_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -12,6 +13,8 @@ import 'package:litetest/litetest.dart'; import 'package:vm_service/vm_service.dart' as vms; import 'package:vm_service/vm_service_io.dart'; +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); + void main() { test('Setting invalid directory returns an error', () async { vms.VmService? vmService; @@ -59,7 +62,7 @@ void main() { 'ext.ui.window.impellerEnabled', isolateId: isolateId, ); - expect(response.json!['enabled'], false); + expect(response.json!['enabled'], impellerEnabled); } finally { await vmService?.dispose(); } diff --git a/testing/resources/impeller_canvas_test_toImage.png b/testing/resources/impeller_canvas_test_toImage.png new file mode 100644 index 0000000000000000000000000000000000000000..3c1c0f0bed47fb48bd77d1f08f08fdba165f3a3b GIT binary patch literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^DImEakt zG3V{=z`Q005!MTt=3&#_%9m+xmY;eg!clNu%qh#0Ek^{5iqa>TJ7oh60D%Lx^4aAN zPCeZ8cAeetT{-^K-+nx2r?Vz|zxpqmjk2>7#IA0AsmNn`@XUr3fu@-&jd&LNeCac2 z`Lv|GO>qkQH4bMD=4(8Q^J{B2b6#zIxbw%4^IlpF?zLia)-Q`DUYQ;^Rarf??2DP9 z_o_6HM;&*r@KMn>7di+UMym>Fp zV^3H0=Pkb;hn3hSwY99|c=DxU`c;+hYqzGg9l05;@bT)dLj~-7xqnqBE!-9@CLSO1 zReI8or^3I#wA-&Z6ml=MR$Q)n)slTZpC&lZcx9!Mn4xkMej(*Nb3%l&;~wR_L2U0VhTAvn3<5mdKI;Vst0G#`{-2eap literal 0 HcmV?d00001 diff --git a/vulkan/vulkan_window.cc b/vulkan/vulkan_window.cc index f7f51dce9e738..345919b83d8c1 100644 --- a/vulkan/vulkan_window.cc +++ b/vulkan/vulkan_window.cc @@ -118,6 +118,7 @@ GrDirectContext* VulkanWindow::GetSkiaGrContext() { } bool VulkanWindow::CreateSkiaGrContext() { +#ifdef SK_VUKLAN GrVkBackendContext backend_context; if (!CreateSkiaBackendContext(&backend_context)) { @@ -138,6 +139,9 @@ bool VulkanWindow::CreateSkiaGrContext() { skia_gr_context_ = context; return true; +#else + return false; +#endif // SK_VULKAN } bool VulkanWindow::CreateSkiaBackendContext(GrVkBackendContext* context) { From af399e7243cf3b477adeaede4b9c99462b30bdaf Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 28 Sep 2023 16:08:49 -0700 Subject: [PATCH 02/22] Update run_tests.py --- testing/run_tests.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index 9a0e24f4f1022..b2521ca17ac10 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -581,6 +581,7 @@ def gather_dart_test( build_dir, dart_file, multithreaded, + enable_impeller=False, enable_observatory=False, expect_failure=False, ): @@ -602,6 +603,9 @@ def gather_dart_test( dart_file_contents.close() command_args.extend(custom_options) + if (enable_impeller): + command_args += [ '--enable-impeller' ] + command_args += [ '--use-test-fonts', '--icu-data-file-path=%s' % os.path.join(build_dir, 'icudtl.dat'), @@ -850,8 +854,10 @@ def gather_dart_tests(build_dir, test_filter): logger.info( "Gathering dart test '%s' with observatory enabled", dart_test_file ) - yield gather_dart_test(build_dir, dart_test_file, True, True) - yield gather_dart_test(build_dir, dart_test_file, False, True) + yield gather_dart_test(build_dir, dart_test_file, True, False, True) + yield gather_dart_test(build_dir, dart_test_file, True, True, True) + yield gather_dart_test(build_dir, dart_test_file, False, False, True) + yield gather_dart_test(build_dir, dart_test_file, False, True, True) for dart_test_file in dart_tests: if test_filter is not None and os.path.basename(dart_test_file @@ -859,8 +865,10 @@ def gather_dart_tests(build_dir, test_filter): logger.info("Skipping '%s' due to filter.", dart_test_file) else: logger.info("Gathering dart test '%s'", dart_test_file) - yield gather_dart_test(build_dir, dart_test_file, True) - yield gather_dart_test(build_dir, dart_test_file, False) + yield gather_dart_test(build_dir, dart_test_file, True, False) + yield gather_dart_test(build_dir, dart_test_file, True, True) + yield gather_dart_test(build_dir, dart_test_file, False, False) + yield gather_dart_test(build_dir, dart_test_file, False, True) def gather_dart_smoke_test(build_dir, test_filter): From 28f759b06b1cb3697d509ad3fd7d06679a08d4f6 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 28 Sep 2023 16:11:51 -0700 Subject: [PATCH 03/22] Update run_tests.py --- testing/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index b2521ca17ac10..d3e91a731f231 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -604,7 +604,7 @@ def gather_dart_test( command_args.extend(custom_options) if (enable_impeller): - command_args += [ '--enable-impeller' ] + command_args += ['--enable-impeller'] command_args += [ '--use-test-fonts', From c91fb441b3f40f2e8de759e5d48b7be3538403a1 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 29 Sep 2023 10:16:44 -0700 Subject: [PATCH 04/22] missing guard --- shell/testing/tester_main.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index c4ac7dfb8902f..3ecff86290006 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -136,6 +136,7 @@ class TesterPlatformView : public PlatformView, // |PlatformView| std::unique_ptr CreateRenderingSurface() override { +#if IMPELLER_SUPPORTS_RENDERING if (delegate_.OnPlatformViewGetSettings().enable_impeller) { FML_DCHECK(impeller_context_); auto surface = @@ -143,6 +144,7 @@ class TesterPlatformView : public PlatformView, FML_DCHECK(surface->IsValid()); return surface; } +#endif // IMPELLER_SUPPORTS_RENDERING FML_DCHECK(!impeller_context_); auto surface = std::make_unique( this, true /* render to surface */); From 0e493305e18fafd7b513e5fef143a80003818840 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 29 Sep 2023 10:22:49 -0700 Subject: [PATCH 05/22] windows build --- shell/testing/BUILD.gn | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/shell/testing/BUILD.gn b/shell/testing/BUILD.gn index 9903a9356b61f..7cbbb94e694dc 100644 --- a/shell/testing/BUILD.gn +++ b/shell/testing/BUILD.gn @@ -22,8 +22,9 @@ executable("testing") { ] sources = [ "tester_main.cc" ] + libs = [] if (is_win) { - libs = [ + libs += [ "psapi.lib", "user32.lib", "FontSub.lib", @@ -53,11 +54,11 @@ executable("testing") { ] if (is_win) { - libs = [ "$root_out_dir/vk_swiftshader.dll" ] + libs += [ "$root_out_dir/vk_swiftshader.dll" ] } else if (is_mac) { - libs = [ "$root_out_dir/libvk_swiftshader.dylib" ] + libs += [ "$root_out_dir/libvk_swiftshader.dylib" ] } else if (is_linux) { - libs = [ "$root_out_dir/libvk_swiftshader.so" ] + libs += [ "$root_out_dir/libvk_swiftshader.so" ] } else { assert( false, From bf08e661adcd4d5b52252eafcb46e3fec2b1c91f Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 29 Sep 2023 10:33:06 -0700 Subject: [PATCH 06/22] include --- shell/testing/tester_main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 3ecff86290006..339a25627849a 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "impeller/renderer/surface.h" #define FML_USED_ON_EMBEDDER #include @@ -38,6 +37,7 @@ #include "impeller/renderer/backend/vulkan/context_vk.h" // nogncheck #include "impeller/renderer/backend/vulkan/surface_context_vk.h" // nogncheck #include "impeller/renderer/context.h" // nogncheck +#include "impeller/renderer/surface.h" // nogncheck #include "impeller/renderer/vk/compute_shaders_vk.h" // nogncheck #include "shell/gpu/gpu_surface_vulkan_impeller.h" // nogncheck #if IMPELLER_ENABLE_3D From d4aef8a67bd78fdb618866bd06385c0b0fc1d7b2 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 29 Sep 2023 11:10:41 -0700 Subject: [PATCH 07/22] shutdown, fix non-impeller build --- shell/testing/tester_main.cc | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 339a25627849a..820e528ca1fcd 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -120,15 +120,24 @@ class TesterGPUSurfaceSoftware : public GPUSurfaceSoftware { class TesterPlatformView : public PlatformView, public GPUSurfaceSoftwareDelegate { public: - TesterPlatformView(Delegate& delegate, - const TaskRunners& task_runners, - const std::shared_ptr& impeller_context, - const std::shared_ptr& - impeller_surface_context) + TesterPlatformView( + Delegate& delegate, + const TaskRunners& task_runners, + const std::shared_ptr& impeller_context, + const std::shared_ptr& + impeller_surface_context) : PlatformView(delegate, task_runners), impeller_context_(impeller_context), impeller_surface_context_(impeller_surface_context) {} + ~TesterPlatformView() { +#if IMPELLER_SUPPORTS_RENDERING + if (impeller_context_) { + impeller_context_->Shutdown(); + } +#endif + } + // |PlatformView| std::shared_ptr GetImpellerContext() const override { return impeller_context_; From ee377fc9e1730545f8227d2a5219e040b9c56dad Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 29 Sep 2023 16:46:43 -0700 Subject: [PATCH 08/22] fuchsia builds --- shell/testing/tester_main.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 820e528ca1fcd..b1bdedbd754be 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -140,7 +140,7 @@ class TesterPlatformView : public PlatformView, // |PlatformView| std::shared_ptr GetImpellerContext() const override { - return impeller_context_; + return std::static_pointer_cast(impeller_context_); } // |PlatformView| @@ -195,7 +195,7 @@ class TesterPlatformView : public PlatformView, private: sk_sp sk_surface_ = nullptr; - std::shared_ptr impeller_context_; + std::shared_ptr impeller_context_; std::shared_ptr impeller_surface_context_; std::shared_ptr external_view_embedder_ = std::make_shared(); @@ -321,7 +321,7 @@ int RunTester(const flutter::Settings& settings, impeller_context = impeller::ContextVK::Create(std::move(context_settings)); if (!impeller_context || !impeller_context->IsValid()) { VALIDATION_LOG << "Could not create Vulkan context."; - return -1; + return EXIT_FAILURE; } impeller::vk::SurfaceKHR vk_surface; @@ -334,7 +334,7 @@ int RunTester(const flutter::Settings& settings, if (res != impeller::vk::Result::eSuccess) { VALIDATION_LOG << "Could not create surface for tester " << impeller::vk::to_string(res); - return -1; + return EXIT_FAILURE; } impeller::vk::UniqueSurfaceKHR surface{vk_surface, @@ -342,7 +342,7 @@ int RunTester(const flutter::Settings& settings, impeller_surface_context = impeller_context->CreateSurfaceContext(); if (!impeller_surface_context->SetWindowSurface(std::move(surface))) { VALIDATION_LOG << "Could not set up surface for context."; - return -1; + return EXIT_FAILURE; } } #endif // IMPELLER_SUPPORTS_RENDERING From 4f060763aeed8fec2621642549a1d3bce2f49e4a Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 3 Oct 2023 13:21:14 -0700 Subject: [PATCH 09/22] one more guard for fuchsia --- shell/testing/tester_main.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index b1bdedbd754be..ec0019467a50f 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -140,7 +140,11 @@ class TesterPlatformView : public PlatformView, // |PlatformView| std::shared_ptr GetImpellerContext() const override { +#if IMPELLER_SUPPORTS_RENDERING return std::static_pointer_cast(impeller_context_); +#else + return nullptr; +#endif // IMPELLER_SUPPORTS_RENDERING } // |PlatformView| From de4abfa18420c9f71b0fe935f96a041c346c4d1c Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 4 Oct 2023 10:28:42 -0700 Subject: [PATCH 10/22] more skips for linux, specify pixel format --- .../renderer/backend/vulkan/blit_command_vk_unittests.cc | 5 +++++ testing/dart/canvas_test.dart | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/impeller/renderer/backend/vulkan/blit_command_vk_unittests.cc b/impeller/renderer/backend/vulkan/blit_command_vk_unittests.cc index 9b911c66cef17..260c9e4a45861 100644 --- a/impeller/renderer/backend/vulkan/blit_command_vk_unittests.cc +++ b/impeller/renderer/backend/vulkan/blit_command_vk_unittests.cc @@ -16,9 +16,11 @@ TEST(BlitCommandVkTest, BlitCopyTextureToTextureCommandVK) { auto encoder = std::make_unique(context)->Create(); BlitCopyTextureToTextureCommandVK cmd; cmd.source = context->GetResourceAllocator()->CreateTexture({ + .format = PixelFormat::kR8G8B8A8UNormInt, .size = ISize(100, 100), }); cmd.destination = context->GetResourceAllocator()->CreateTexture({ + .format = PixelFormat::kR8G8B8A8UNormInt, .size = ISize(100, 100), }); bool result = cmd.Encode(*encoder.get()); @@ -32,6 +34,7 @@ TEST(BlitCommandVkTest, BlitCopyTextureToBufferCommandVK) { auto encoder = std::make_unique(context)->Create(); BlitCopyTextureToBufferCommandVK cmd; cmd.source = context->GetResourceAllocator()->CreateTexture({ + .format = PixelFormat::kR8G8B8A8UNormInt, .size = ISize(100, 100), }); cmd.destination = context->GetResourceAllocator()->CreateBuffer({ @@ -48,6 +51,7 @@ TEST(BlitCommandVkTest, BlitCopyBufferToTextureCommandVK) { auto encoder = std::make_unique(context)->Create(); BlitCopyBufferToTextureCommandVK cmd; cmd.destination = context->GetResourceAllocator()->CreateTexture({ + .format = PixelFormat::kR8G8B8A8UNormInt, .size = ISize(100, 100), }); cmd.source = context->GetResourceAllocator() @@ -66,6 +70,7 @@ TEST(BlitCommandVkTest, BlitGenerateMipmapCommandVK) { auto encoder = std::make_unique(context)->Create(); BlitGenerateMipmapCommandVK cmd; cmd.texture = context->GetResourceAllocator()->CreateTexture({ + .format = PixelFormat::kR8G8B8A8UNormInt, .size = ISize(100, 100), .mip_count = 2, }); diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index c0aba82e63c3a..c7368c7558aca 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -220,7 +220,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'canvas_test_gradient.png'); expect(areEqual, true); - }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('Simple dithered gradient', () async { // TODO(matanl): Reword this test once we remove the deprecated API. @@ -236,7 +236,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png'); expect(areEqual, true); - }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('Null values allowed for drawAtlas methods', () async { final Image image = await createImage(100, 100); @@ -372,7 +372,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'dotted_path_effect_mixed_with_stroked_geometry.png'); expect(areEqual, true); - }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('Gradients with matrices in Paragraphs render correctly', () async { final Image image = await toImage((Canvas canvas) { @@ -424,7 +424,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'text_with_gradient_with_matrix.png'); expect(areEqual, true); - }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('toImageSync - too big', () async { PictureRecorder recorder = PictureRecorder(); From 5975e1412d6d4d305d0abadcb4ad32f5be89879b Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 4 Oct 2023 12:11:00 -0700 Subject: [PATCH 11/22] fix condition --- testing/dart/canvas_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index c7368c7558aca..8ef1bbe7af5b1 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -220,7 +220,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'canvas_test_gradient.png'); expect(areEqual, true); - }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('Simple dithered gradient', () async { // TODO(matanl): Reword this test once we remove the deprecated API. @@ -236,7 +236,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png'); expect(areEqual, true); - }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('Null values allowed for drawAtlas methods', () async { final Image image = await createImage(100, 100); @@ -372,7 +372,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'dotted_path_effect_mixed_with_stroked_geometry.png'); expect(areEqual, true); - }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('Gradients with matrices in Paragraphs render correctly', () async { final Image image = await toImage((Canvas canvas) { @@ -424,7 +424,7 @@ void main() { final bool areEqual = await fuzzyGoldenImageCompare(image, 'text_with_gradient_with_matrix.png'); expect(areEqual, true); - }, skip: !Platform.isLinux && !impellerEnabled); // https://github.com/flutter/flutter/issues/53784 + }, skip: !Platform.isLinux || impellerEnabled); // https://github.com/flutter/flutter/issues/53784 test('toImageSync - too big', () async { PictureRecorder recorder = PictureRecorder(); From 11d53f2208dd88c826fc627aba4552f15f1f73d3 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 9 Oct 2023 16:24:18 -0700 Subject: [PATCH 12/22] make swiftshader lookup dynamic --- shell/testing/BUILD.gn | 12 ------------ shell/testing/tester_main.cc | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/shell/testing/BUILD.gn b/shell/testing/BUILD.gn index 7cbbb94e694dc..8f4572310c519 100644 --- a/shell/testing/BUILD.gn +++ b/shell/testing/BUILD.gn @@ -52,18 +52,6 @@ executable("testing") { "//flutter/impeller", "//third_party/swiftshader", ] - - if (is_win) { - libs += [ "$root_out_dir/vk_swiftshader.dll" ] - } else if (is_mac) { - libs += [ "$root_out_dir/libvk_swiftshader.dylib" ] - } else if (is_linux) { - libs += [ "$root_out_dir/libvk_swiftshader.so" ] - } else { - assert( - false, - "Only Windows, Mac, and Linux are supported for this target with Impeller.") - } } metadata = { diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index ec0019467a50f..11eb68d389685 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -60,6 +60,14 @@ static std::vector> ShaderLibraryMappings() { }; } +#if FML_OS_MACOSX +#define VULKAN_SO_PATH "libvk_swiftshader.dylib" +#elif FML_OS_WIN +#define VULKAN_SO_PATH "vk_swiftshader.dll" +#else +#define VULKAN_SO_PATH "libvk_swiftshader.so" +#endif + #else namespace impeller { class Context; @@ -315,9 +323,15 @@ int RunTester(const flutter::Settings& settings, #if IMPELLER_SUPPORTS_RENDERING if (settings.enable_impeller) { + auto proc_table = + fml::MakeRefCounted(VULKAN_SO_PATH); + if (!proc_table->NativeGetInstanceProcAddr()) { + FML_LOG(ERROR) << "Could not load Swiftshader library."; + return EXIT_FAILURE; + } impeller::ContextVK::Settings context_settings; context_settings.proc_address_callback = - reinterpret_cast(&::vkGetInstanceProcAddr); + proc_table->NativeGetInstanceProcAddr(); context_settings.shader_libraries_data = ShaderLibraryMappings(); context_settings.cache_directory = fml::paths::GetCachesDirectory(); context_settings.enable_validation = settings.enable_vulkan_validation; From 79f99c9b8d479c37744574760283024a624eb674 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 9 Oct 2023 20:15:50 -0700 Subject: [PATCH 13/22] nits --- ci/licenses_golden/licenses_flutter | 2 + .../common/shell_test_platform_view_vulkan.cc | 6 +- shell/testing/tester_main.cc | 10 +--- testing/dart/canvas_test.dart | 4 +- testing/dart/codec_test.dart | 2 +- testing/dart/color_filter_test.dart | 3 +- testing/dart/encoding_test.dart | 2 +- testing/dart/fragment_shader_test.dart | 2 +- testing/dart/gpu_test.dart | 4 +- testing/dart/image_filter_test.dart | 3 +- testing/dart/impeller_enabled.dart | 7 +++ testing/dart/observatory/skp_test.dart | 3 +- testing/dart/observatory/tracing_test.dart | 3 +- .../observatory/vmservice_methods_test.dart | 2 +- testing/run_tests.py | 56 +++++++++++++++---- testing/test_vulkan_context.cc | 9 +-- vulkan/swiftshader_path.h | 18 ++++++ 17 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 testing/dart/impeller_enabled.dart create mode 100644 vulkan/swiftshader_path.h diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 84d3c9fd3f98a..52f0622fa7e7b 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -3404,6 +3404,7 @@ ORIGIN: ../../../flutter/vulkan/procs/vulkan_interface.cc + ../../../flutter/LIC ORIGIN: ../../../flutter/vulkan/procs/vulkan_interface.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/vulkan/procs/vulkan_proc_table.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/vulkan/procs/vulkan_proc_table.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/vulkan/swiftshader_path.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/vulkan/vulkan_application.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/vulkan/vulkan_application.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/vulkan/vulkan_backbuffer.cc + ../../../flutter/LICENSE @@ -6184,6 +6185,7 @@ FILE: ../../../flutter/vulkan/procs/vulkan_interface.cc FILE: ../../../flutter/vulkan/procs/vulkan_interface.h FILE: ../../../flutter/vulkan/procs/vulkan_proc_table.cc FILE: ../../../flutter/vulkan/procs/vulkan_proc_table.h +FILE: ../../../flutter/vulkan/swiftshader_path.h FILE: ../../../flutter/vulkan/vulkan_application.cc FILE: ../../../flutter/vulkan/vulkan_application.h FILE: ../../../flutter/vulkan/vulkan_backbuffer.cc diff --git a/shell/common/shell_test_platform_view_vulkan.cc b/shell/common/shell_test_platform_view_vulkan.cc index 38170118e2072..d19e5cb2bcaff 100644 --- a/shell/common/shell_test_platform_view_vulkan.cc +++ b/shell/common/shell_test_platform_view_vulkan.cc @@ -17,12 +17,8 @@ #if OS_FUCHSIA #define VULKAN_SO_PATH "libvulkan.so" -#elif FML_OS_MACOSX -#define VULKAN_SO_PATH "libvk_swiftshader.dylib" -#elif FML_OS_WIN -#define VULKAN_SO_PATH "vk_swiftshader.dll" #else -#define VULKAN_SO_PATH "libvk_swiftshader.so" +#include "flutter/vulkan/swiftshader_path.h" #endif namespace flutter { diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 11eb68d389685..7c28f747d5317 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -31,6 +31,7 @@ #if IMPELLER_SUPPORTS_RENDERING #include // nogncheck +#include "flutter/vulkan/swiftshader_path.h" // nogncheck #include "impeller/core/formats.h" // nogncheck #include "impeller/entity/vk/entity_shaders_vk.h" // nogncheck #include "impeller/entity/vk/modern_shaders_vk.h" // nogncheck @@ -59,15 +60,6 @@ static std::vector> ShaderLibraryMappings() { impeller_compute_shaders_vk_length), }; } - -#if FML_OS_MACOSX -#define VULKAN_SO_PATH "libvk_swiftshader.dylib" -#elif FML_OS_WIN -#define VULKAN_SO_PATH "vk_swiftshader.dll" -#else -#define VULKAN_SO_PATH "libvk_swiftshader.so" -#endif - #else namespace impeller { class Context; diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 8ef1bbe7af5b1..e04bb13c6be00 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -12,9 +12,9 @@ import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; import 'package:vector_math/vector_math_64.dart'; -typedef CanvasCallback = void Function(Canvas canvas); +import 'impeller_enabled.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +typedef CanvasCallback = void Function(Canvas canvas); Future createImage(int width, int height) { final Completer completer = Completer(); diff --git a/testing/dart/codec_test.dart b/testing/dart/codec_test.dart index 365e1ddeffb49..702a5fd14e016 100644 --- a/testing/dart/codec_test.dart +++ b/testing/dart/codec_test.dart @@ -9,7 +9,7 @@ import 'dart:ui' as ui; import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import 'impeller_enabled.dart'; void main() { diff --git a/testing/dart/color_filter_test.dart b/testing/dart/color_filter_test.dart index 41a8d86067338..66ec9548ca58f 100644 --- a/testing/dart/color_filter_test.dart +++ b/testing/dart/color_filter_test.dart @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import 'impeller_enabled.dart'; const Color transparent = Color(0x00000000); const Color red = Color(0xFFAA0000); diff --git a/testing/dart/encoding_test.dart b/testing/dart/encoding_test.dart index 5b0ce19834368..2aa426a108100 100644 --- a/testing/dart/encoding_test.dart +++ b/testing/dart/encoding_test.dart @@ -10,7 +10,7 @@ import 'dart:ui'; import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import 'impeller_enabled.dart'; const int _kWidth = 10; const int _kRadius = 2; diff --git a/testing/dart/fragment_shader_test.dart b/testing/dart/fragment_shader_test.dart index 56837c89aa04e..eef157267f174 100644 --- a/testing/dart/fragment_shader_test.dart +++ b/testing/dart/fragment_shader_test.dart @@ -14,7 +14,7 @@ import 'package:path/path.dart' as path; import 'shader_test_file_utils.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import 'impeller_enabled.dart'; void main() async { bool assertsEnabled = false; diff --git a/testing/dart/gpu_test.dart b/testing/dart/gpu_test.dart index d4925e6b54d3a..fc3c0c5fff951 100644 --- a/testing/dart/gpu_test.dart +++ b/testing/dart/gpu_test.dart @@ -4,12 +4,10 @@ // ignore_for_file: avoid_relative_lib_imports -import 'dart:io'; - import 'package:litetest/litetest.dart'; import '../../lib/gpu/lib/gpu.dart' as gpu; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import 'impeller_enabled.dart'; void main() { // TODO(131346): Remove this once we migrate the Dart GPU API into this space. diff --git a/testing/dart/image_filter_test.dart b/testing/dart/image_filter_test.dart index 9142362845ebc..0a79513d99430 100644 --- a/testing/dart/image_filter_test.dart +++ b/testing/dart/image_filter_test.dart @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import 'impeller_enabled.dart'; const Color red = Color(0xFFAA0000); const Color green = Color(0xFF00AA00); diff --git a/testing/dart/impeller_enabled.dart b/testing/dart/impeller_enabled.dart new file mode 100644 index 0000000000000..18c457e3c059f --- /dev/null +++ b/testing/dart/impeller_enabled.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); diff --git a/testing/dart/observatory/skp_test.dart b/testing/dart/observatory/skp_test.dart index 2414f39faee3c..6c2a1460f9f21 100644 --- a/testing/dart/observatory/skp_test.dart +++ b/testing/dart/observatory/skp_test.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; @@ -13,7 +12,7 @@ import 'package:litetest/litetest.dart'; import 'package:vm_service/vm_service.dart' as vms; import 'package:vm_service/vm_service_io.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import '../impeller_enabled.dart'; void main() { test('Capture an SKP ', () async { diff --git a/testing/dart/observatory/tracing_test.dart b/testing/dart/observatory/tracing_test.dart index ea0674934a1fe..6372a6a159763 100644 --- a/testing/dart/observatory/tracing_test.dart +++ b/testing/dart/observatory/tracing_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert' show base64Decode; -import 'dart:io'; import 'dart:developer' as developer; import 'dart:ui'; @@ -13,7 +12,7 @@ import 'package:vm_service/vm_service.dart' as vms; import 'package:vm_service/vm_service_io.dart'; import 'package:vm_service_protos/vm_service_protos.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import '../impeller_enabled.dart'; Future _testChromeFormatTrace(vms.VmService vmService) async { final vms.Timeline timeline = await vmService.getVMTimeline(); diff --git a/testing/dart/observatory/vmservice_methods_test.dart b/testing/dart/observatory/vmservice_methods_test.dart index cd9c4ec93b15c..717e7fbed7e31 100644 --- a/testing/dart/observatory/vmservice_methods_test.dart +++ b/testing/dart/observatory/vmservice_methods_test.dart @@ -13,7 +13,7 @@ import 'package:litetest/litetest.dart'; import 'package:vm_service/vm_service.dart' as vms; import 'package:vm_service/vm_service_io.dart'; -bool get impellerEnabled => Platform.executableArguments.contains('--enable-impeller'); +import '../impeller_enabled.dart'; void main() { test('Setting invalid directory returns an error', () async { diff --git a/testing/run_tests.py b/testing/run_tests.py index d3e91a731f231..31d1193152595 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -854,10 +854,34 @@ def gather_dart_tests(build_dir, test_filter): logger.info( "Gathering dart test '%s' with observatory enabled", dart_test_file ) - yield gather_dart_test(build_dir, dart_test_file, True, False, True) - yield gather_dart_test(build_dir, dart_test_file, True, True, True) - yield gather_dart_test(build_dir, dart_test_file, False, False, True) - yield gather_dart_test(build_dir, dart_test_file, False, True, True) + yield gather_dart_test( + build_dir, + dart_test_file, + multithreaded=True, + enable_impeller=False, + enable_observatory=True, + ) + yield gather_dart_test( + build_dir, + dart_test_file, + multithreaded=True, + enable_impeller=True, + enable_observatory=True + ) + yield gather_dart_test( + build_dir, + dart_test_file, + multithreaded=False, + enable_impeller=False, + enable_observatory=True + ) + yield gather_dart_test( + build_dir, + dart_test_file, + multithreaded=False, + enable_impeller=True, + enable_observatory=True + ) for dart_test_file in dart_tests: if test_filter is not None and os.path.basename(dart_test_file @@ -865,10 +889,18 @@ def gather_dart_tests(build_dir, test_filter): logger.info("Skipping '%s' due to filter.", dart_test_file) else: logger.info("Gathering dart test '%s'", dart_test_file) - yield gather_dart_test(build_dir, dart_test_file, True, False) - yield gather_dart_test(build_dir, dart_test_file, True, True) - yield gather_dart_test(build_dir, dart_test_file, False, False) - yield gather_dart_test(build_dir, dart_test_file, False, True) + yield gather_dart_test( + build_dir, dart_test_file, multithreaded=True, enable_impeller=False + ) + yield gather_dart_test( + build_dir, dart_test_file, multithreaded=True, enable_impeller=True + ) + yield gather_dart_test( + build_dir, dart_test_file, multithreaded=False, enable_impeller=False + ) + yield gather_dart_test( + build_dir, dart_test_file, multithreaded=False, enable_impeller=True + ) def gather_dart_smoke_test(build_dir, test_filter): @@ -883,8 +915,12 @@ def gather_dart_smoke_test(build_dir, test_filter): ) not in test_filter: logger.info("Skipping '%s' due to filter.", smoke_test) else: - yield gather_dart_test(build_dir, smoke_test, True, expect_failure=True) - yield gather_dart_test(build_dir, smoke_test, False, expect_failure=True) + yield gather_dart_test( + build_dir, smoke_test, multithreaded=True, expect_failure=True + ) + yield gather_dart_test( + build_dir, smoke_test, multithreaded=False, expect_failure=True + ) def gather_dart_package_tests(build_dir, package_path, extra_opts): diff --git a/testing/test_vulkan_context.cc b/testing/test_vulkan_context.cc index ea36581add8e2..5652431a800ad 100644 --- a/testing/test_vulkan_context.cc +++ b/testing/test_vulkan_context.cc @@ -14,19 +14,12 @@ #include "flutter/fml/memory/ref_ptr.h" #include "flutter/fml/native_library.h" +#include "flutter/vulkan/swiftshader_path.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" -#elif FML_OS_WIN -#define VULKAN_SO_PATH "vk_swiftshader.dll" -#else -#define VULKAN_SO_PATH "libvk_swiftshader.so" -#endif - namespace flutter { namespace testing { diff --git a/vulkan/swiftshader_path.h b/vulkan/swiftshader_path.h new file mode 100644 index 0000000000000..17e31de206fa9 --- /dev/null +++ b/vulkan/swiftshader_path.h @@ -0,0 +1,18 @@ +// 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_VULKAN_SWIFTSHADER_PATH_H_ +#define FLUTTER_VULKAN_SWIFTSHADER_PATH_H_ + +#ifndef VULKAN_SO_PATH +#if FML_OS_MACOSX +#define VULKAN_SO_PATH "libvk_swiftshader.dylib" +#elif FML_OS_WIN +#define VULKAN_SO_PATH "vk_swiftshader.dll" +#elif FML_OS_LINUX +#define VULKAN_SO_PATH "libvk_swiftshader.so" +#endif // FML_OS_LINUX +#endif // VULKAN_SO_PATH + +#endif // FLUTTER_VULKAN_SWIFTSHADER_PATH_H_ From 7cd05f67744c653beb960e0531a55e917f89a92e Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 10 Oct 2023 11:12:11 -0700 Subject: [PATCH 14/22] fix header --- vulkan/swiftshader_path.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulkan/swiftshader_path.h b/vulkan/swiftshader_path.h index 17e31de206fa9..424116a7efec7 100644 --- a/vulkan/swiftshader_path.h +++ b/vulkan/swiftshader_path.h @@ -10,9 +10,9 @@ #define VULKAN_SO_PATH "libvk_swiftshader.dylib" #elif FML_OS_WIN #define VULKAN_SO_PATH "vk_swiftshader.dll" -#elif FML_OS_LINUX +#else #define VULKAN_SO_PATH "libvk_swiftshader.so" -#endif // FML_OS_LINUX +#endif // !FML_OS_MACOSX && !FML_OS_WIN #endif // VULKAN_SO_PATH #endif // FLUTTER_VULKAN_SWIFTSHADER_PATH_H_ From effb21b8a54a019004ff404d9183db3e1d665745 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 10 Oct 2023 13:52:02 -0700 Subject: [PATCH 15/22] Keep swiftshader lib open --- shell/testing/tester_main.cc | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 7c28f747d5317..0673d3291e00c 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -41,6 +41,7 @@ #include "impeller/renderer/surface.h" // nogncheck #include "impeller/renderer/vk/compute_shaders_vk.h" // nogncheck #include "shell/gpu/gpu_surface_vulkan_impeller.h" // nogncheck +#include "vulkan/procs/vulkan_proc_table.h" #if IMPELLER_ENABLE_3D #include "impeller/scene/shaders/vk/scene_shaders_vk.h" // nogncheck #endif // IMPELLER_ENABLE_3D @@ -66,6 +67,10 @@ class Context; class ContextVK; class SurfaceContextVK; } // namespace impeller + +namespace vulkan { +class VulkanProcTable; +} #endif // IMPELLER_SUPPORTS_RENDERING #if defined(FML_OS_WIN) @@ -125,8 +130,10 @@ class TesterPlatformView : public PlatformView, const TaskRunners& task_runners, const std::shared_ptr& impeller_context, const std::shared_ptr& - impeller_surface_context) + impeller_surface_context, + const fml::RefPtr& vulkan_proc_table) : PlatformView(delegate, task_runners), + vulkan_proc_table_(vulkan_proc_table), impeller_context_(impeller_context), impeller_surface_context_(impeller_surface_context) {} @@ -199,6 +206,7 @@ class TesterPlatformView : public PlatformView, private: sk_sp sk_surface_ = nullptr; + fml::RefPtr vulkan_proc_table_; std::shared_ptr impeller_context_; std::shared_ptr impeller_surface_context_; std::shared_ptr external_view_embedder_ = @@ -312,18 +320,19 @@ int RunTester(const flutter::Settings& settings, std::shared_ptr impeller_context; std::shared_ptr impeller_surface_context; + fml::RefPtr vulkan_proc_table; #if IMPELLER_SUPPORTS_RENDERING if (settings.enable_impeller) { - auto proc_table = + vulkan_proc_table = fml::MakeRefCounted(VULKAN_SO_PATH); - if (!proc_table->NativeGetInstanceProcAddr()) { + if (!vulkan_proc_table->NativeGetInstanceProcAddr()) { FML_LOG(ERROR) << "Could not load Swiftshader library."; return EXIT_FAILURE; } impeller::ContextVK::Settings context_settings; context_settings.proc_address_callback = - proc_table->NativeGetInstanceProcAddr(); + vulkan_proc_table->NativeGetInstanceProcAddr(); context_settings.shader_libraries_data = ShaderLibraryMappings(); context_settings.cache_directory = fml::paths::GetCachesDirectory(); context_settings.enable_validation = settings.enable_vulkan_validation; @@ -358,10 +367,11 @@ int RunTester(const flutter::Settings& settings, #endif // IMPELLER_SUPPORTS_RENDERING Shell::CreateCallback on_create_platform_view = - [impeller_context, impeller_surface_context](Shell& shell) { + [impeller_context, impeller_surface_context, + vulkan_proc_table](Shell& shell) { return std::make_unique( shell, shell.GetTaskRunners(), impeller_context, - impeller_surface_context); + impeller_surface_context, vulkan_proc_table); }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { From 28eea9a955190016107e76e7b823984156a4db1b Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 10 Oct 2023 15:34:43 -0700 Subject: [PATCH 16/22] fix --- shell/testing/tester_main.cc | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 7c28f747d5317..4d660a26542ca 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -24,6 +24,7 @@ #include "flutter/shell/common/switches.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/gpu/gpu_surface_software.h" +#include "vulkan/procs/vulkan_proc_table.h" #include "third_party/dart/runtime/include/bin/dart_io_api.h" #include "third_party/dart/runtime/include/dart_api.h" @@ -125,8 +126,10 @@ class TesterPlatformView : public PlatformView, const TaskRunners& task_runners, const std::shared_ptr& impeller_context, const std::shared_ptr& - impeller_surface_context) + impeller_surface_context, + const fml::RefPtr& vulkan_proc_table) : PlatformView(delegate, task_runners), + vulkan_proc_table_(vulkan_proc_table), impeller_context_(impeller_context), impeller_surface_context_(impeller_surface_context) {} @@ -199,6 +202,7 @@ class TesterPlatformView : public PlatformView, private: sk_sp sk_surface_ = nullptr; + fml::RefPtr vulkan_proc_table_; std::shared_ptr impeller_context_; std::shared_ptr impeller_surface_context_; std::shared_ptr external_view_embedder_ = @@ -312,18 +316,19 @@ int RunTester(const flutter::Settings& settings, std::shared_ptr impeller_context; std::shared_ptr impeller_surface_context; + fml::RefPtr vulkan_proc_table; #if IMPELLER_SUPPORTS_RENDERING if (settings.enable_impeller) { - auto proc_table = + vulkan_proc_table = fml::MakeRefCounted(VULKAN_SO_PATH); - if (!proc_table->NativeGetInstanceProcAddr()) { + if (!vulkan_proc_table->NativeGetInstanceProcAddr()) { FML_LOG(ERROR) << "Could not load Swiftshader library."; return EXIT_FAILURE; } impeller::ContextVK::Settings context_settings; context_settings.proc_address_callback = - proc_table->NativeGetInstanceProcAddr(); + vulkan_proc_table->NativeGetInstanceProcAddr(); context_settings.shader_libraries_data = ShaderLibraryMappings(); context_settings.cache_directory = fml::paths::GetCachesDirectory(); context_settings.enable_validation = settings.enable_vulkan_validation; @@ -358,10 +363,11 @@ int RunTester(const flutter::Settings& settings, #endif // IMPELLER_SUPPORTS_RENDERING Shell::CreateCallback on_create_platform_view = - [impeller_context, impeller_surface_context](Shell& shell) { + [impeller_context, impeller_surface_context, + vulkan_proc_table](Shell& shell) { return std::make_unique( shell, shell.GetTaskRunners(), impeller_context, - impeller_surface_context); + impeller_surface_context, vulkan_proc_table); }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { From f801e47f8c26e3a1f0b9aa7b9d9fb0fb8a0dd90d Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 11 Oct 2023 09:44:29 -0700 Subject: [PATCH 17/22] refactor to simpler struct without forward decls --- shell/testing/tester_main.cc | 99 ++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 2c1e93016af95..f95715ae98f25 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -24,7 +24,6 @@ #include "flutter/shell/common/switches.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/gpu/gpu_surface_software.h" -#include "vulkan/procs/vulkan_proc_table.h" #include "third_party/dart/runtime/include/bin/dart_io_api.h" #include "third_party/dart/runtime/include/dart_api.h" @@ -32,17 +31,15 @@ #if IMPELLER_SUPPORTS_RENDERING #include // nogncheck +#include "flutter/vulkan/procs/vulkan_proc_table.h" // nogncheck #include "flutter/vulkan/swiftshader_path.h" // nogncheck -#include "impeller/core/formats.h" // nogncheck #include "impeller/entity/vk/entity_shaders_vk.h" // nogncheck #include "impeller/entity/vk/modern_shaders_vk.h" // nogncheck #include "impeller/renderer/backend/vulkan/context_vk.h" // nogncheck #include "impeller/renderer/backend/vulkan/surface_context_vk.h" // nogncheck #include "impeller/renderer/context.h" // nogncheck -#include "impeller/renderer/surface.h" // nogncheck #include "impeller/renderer/vk/compute_shaders_vk.h" // nogncheck #include "shell/gpu/gpu_surface_vulkan_impeller.h" // nogncheck -#include "vulkan/procs/vulkan_proc_table.h" #if IMPELLER_ENABLE_3D #include "impeller/scene/shaders/vk/scene_shaders_vk.h" // nogncheck #endif // IMPELLER_ENABLE_3D @@ -62,16 +59,14 @@ static std::vector> ShaderLibraryMappings() { impeller_compute_shaders_vk_length), }; } + +struct ImpellerVulkanContextHolder { + fml::RefPtr vulkan_proc_table; + std::shared_ptr context; + std::shared_ptr surface_context; +}; #else -namespace impeller { -class Context; -class ContextVK; -class SurfaceContextVK; -} // namespace impeller - -namespace vulkan { -class VulkanProcTable; -} +struct ImpellerVulkanContextHolder {}; #endif // IMPELLER_SUPPORTS_RENDERING #if defined(FML_OS_WIN) @@ -126,22 +121,16 @@ class TesterGPUSurfaceSoftware : public GPUSurfaceSoftware { class TesterPlatformView : public PlatformView, public GPUSurfaceSoftwareDelegate { public: - TesterPlatformView( - Delegate& delegate, - const TaskRunners& task_runners, - const std::shared_ptr& impeller_context, - const std::shared_ptr& - impeller_surface_context, - const fml::RefPtr& vulkan_proc_table) + TesterPlatformView(Delegate& delegate, + const TaskRunners& task_runners, + ImpellerVulkanContextHolder impeller_context_holder) : PlatformView(delegate, task_runners), - vulkan_proc_table_(vulkan_proc_table), - impeller_context_(impeller_context), - impeller_surface_context_(impeller_surface_context) {} + impeller_context_holder_(std::move(impeller_context_holder)) {} ~TesterPlatformView() { #if IMPELLER_SUPPORTS_RENDERING - if (impeller_context_) { - impeller_context_->Shutdown(); + if (impeller_context_holder_.context) { + impeller_context_holder_.context->Shutdown(); } #endif } @@ -149,7 +138,8 @@ class TesterPlatformView : public PlatformView, // |PlatformView| std::shared_ptr GetImpellerContext() const override { #if IMPELLER_SUPPORTS_RENDERING - return std::static_pointer_cast(impeller_context_); + return std::static_pointer_cast( + impeller_context_holder_.context); #else return nullptr; #endif // IMPELLER_SUPPORTS_RENDERING @@ -159,14 +149,14 @@ class TesterPlatformView : public PlatformView, std::unique_ptr CreateRenderingSurface() override { #if IMPELLER_SUPPORTS_RENDERING if (delegate_.OnPlatformViewGetSettings().enable_impeller) { - FML_DCHECK(impeller_context_); - auto surface = - std::make_unique(impeller_surface_context_); + FML_DCHECK(impeller_context_holder_.context); + auto surface = std::make_unique( + impeller_context_holder_.surface_context); FML_DCHECK(surface->IsValid()); return surface; } #endif // IMPELLER_SUPPORTS_RENDERING - FML_DCHECK(!impeller_context_); + FML_DCHECK(!impeller_context_holder_.context); auto surface = std::make_unique( this, true /* render to surface */); FML_DCHECK(surface->IsValid()); @@ -207,9 +197,7 @@ class TesterPlatformView : public PlatformView, private: sk_sp sk_surface_ = nullptr; - fml::RefPtr vulkan_proc_table_; - std::shared_ptr impeller_context_; - std::shared_ptr impeller_surface_context_; + ImpellerVulkanContextHolder impeller_context_holder_; std::shared_ptr external_view_embedder_ = std::make_shared(); }; @@ -319,48 +307,52 @@ int RunTester(const flutter::Settings& settings, io_task_runner // io ); - std::shared_ptr impeller_context; - std::shared_ptr impeller_surface_context; - fml::RefPtr vulkan_proc_table; + ImpellerVulkanContextHolder impeller_context_holder; #if IMPELLER_SUPPORTS_RENDERING if (settings.enable_impeller) { - vulkan_proc_table = + impeller_context_holder.vulkan_proc_table = fml::MakeRefCounted(VULKAN_SO_PATH); - if (!vulkan_proc_table->NativeGetInstanceProcAddr()) { + if (!impeller_context_holder.vulkan_proc_table + ->NativeGetInstanceProcAddr()) { FML_LOG(ERROR) << "Could not load Swiftshader library."; return EXIT_FAILURE; } impeller::ContextVK::Settings context_settings; context_settings.proc_address_callback = - vulkan_proc_table->NativeGetInstanceProcAddr(); + impeller_context_holder.vulkan_proc_table->NativeGetInstanceProcAddr(); context_settings.shader_libraries_data = ShaderLibraryMappings(); context_settings.cache_directory = fml::paths::GetCachesDirectory(); context_settings.enable_validation = settings.enable_vulkan_validation; - impeller_context = impeller::ContextVK::Create(std::move(context_settings)); - if (!impeller_context || !impeller_context->IsValid()) { + impeller_context_holder.context = + impeller::ContextVK::Create(std::move(context_settings)); + if (!impeller_context_holder.context || + !impeller_context_holder.context->IsValid()) { VALIDATION_LOG << "Could not create Vulkan context."; return EXIT_FAILURE; } impeller::vk::SurfaceKHR vk_surface; impeller::vk::HeadlessSurfaceCreateInfoEXT surface_create_info; - auto res = impeller_context->GetInstance().createHeadlessSurfaceEXT( - &surface_create_info, // surface create info - nullptr, // allocator - &vk_surface // surface - ); + auto res = + impeller_context_holder.context->GetInstance().createHeadlessSurfaceEXT( + &surface_create_info, // surface create info + nullptr, // allocator + &vk_surface // surface + ); if (res != impeller::vk::Result::eSuccess) { VALIDATION_LOG << "Could not create surface for tester " << impeller::vk::to_string(res); return EXIT_FAILURE; } - impeller::vk::UniqueSurfaceKHR surface{vk_surface, - impeller_context->GetInstance()}; - impeller_surface_context = impeller_context->CreateSurfaceContext(); - if (!impeller_surface_context->SetWindowSurface(std::move(surface))) { + impeller::vk::UniqueSurfaceKHR surface{ + vk_surface, impeller_context_holder.context->GetInstance()}; + impeller_context_holder.surface_context = + impeller_context_holder.context->CreateSurfaceContext(); + if (!impeller_context_holder.surface_context->SetWindowSurface( + std::move(surface))) { VALIDATION_LOG << "Could not set up surface for context."; return EXIT_FAILURE; } @@ -368,11 +360,10 @@ int RunTester(const flutter::Settings& settings, #endif // IMPELLER_SUPPORTS_RENDERING Shell::CreateCallback on_create_platform_view = - [impeller_context, impeller_surface_context, - vulkan_proc_table](Shell& shell) { + [impeller_context_holder = + std::move(impeller_context_holder)](Shell& shell) { return std::make_unique( - shell, shell.GetTaskRunners(), impeller_context, - impeller_surface_context, vulkan_proc_table); + shell, shell.GetTaskRunners(), impeller_context_holder); }; Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { From 58b1cb6e98c8658d7c01ba797a5566b2e53e393f Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 11 Oct 2023 10:15:49 -0700 Subject: [PATCH 18/22] Fixes --- impeller/BUILD.gn | 4 ++++ impeller/playground/playground.cc | 2 +- impeller/tools/impeller.gni | 7 +++++++ shell/testing/tester_main.cc | 1 - 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/impeller/BUILD.gn b/impeller/BUILD.gn index c89a89f7f34ea..b3950d9552432 100644 --- a/impeller/BUILD.gn +++ b/impeller/BUILD.gn @@ -34,6 +34,10 @@ config("impeller_public_config") { defines += [ "IMPELLER_ENABLE_VULKAN=1" ] } + if (impeller_enable_vulkan_playgrounds) { + defines += [ "IMPELLER_ENABLE_VULKAN_PLAYGROUNDS=1" ] + } + if (impeller_trace_all_gl_calls) { defines += [ "IMPELLER_TRACE_ALL_GL_CALLS" ] } diff --git a/impeller/playground/playground.cc b/impeller/playground/playground.cc index 93eb4eb3ce204..8d2f591a561dd 100644 --- a/impeller/playground/playground.cc +++ b/impeller/playground/playground.cc @@ -102,7 +102,7 @@ bool Playground::SupportsBackend(PlaygroundBackend backend) { return false; #endif // IMPELLER_ENABLE_OPENGLES case PlaygroundBackend::kVulkan: -#if IMPELLER_ENABLE_VULKAN +#if IMPELLER_ENABLE_VULKAN && IMPELLER_ENABLE_VULKAN_PLAYGROUNDS return true; #else // IMPELLER_ENABLE_VULKAN return false; diff --git a/impeller/tools/impeller.gni b/impeller/tools/impeller.gni index 6d638d402bfcb..cbe7b0a82ff37 100644 --- a/impeller/tools/impeller.gni +++ b/impeller/tools/impeller.gni @@ -25,6 +25,13 @@ declare_args() { impeller_enable_vulkan = (is_linux || is_win || is_android || enable_unittests) && target_os != "fuchsia" + # Whether playgrounds should run with Vulkan. + # + # impeller_enable_vulkan may be true in build environments that run tests but + # do not have a Vulkan ICD present. + impeller_enable_vulkan_playgrounds = + (is_linux || is_win || is_android) && target_os != "fuchsia" + # Whether to use a prebuilt impellerc. # If this is the empty string, impellerc will be built. # If it is non-empty, it should be the absolute path to impellerc. diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index f95715ae98f25..72a8d1d8505ff 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -156,7 +156,6 @@ class TesterPlatformView : public PlatformView, return surface; } #endif // IMPELLER_SUPPORTS_RENDERING - FML_DCHECK(!impeller_context_holder_.context); auto surface = std::make_unique( this, true /* render to surface */); FML_DCHECK(surface->IsValid()); From fb4c806da519bf8fb6004a82cd27bc85e722e550 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 11 Oct 2023 12:32:22 -0700 Subject: [PATCH 19/22] more --- shell/testing/tester_main.cc | 2 +- testing/dart/canvas_test.dart | 4 ++-- .../resources/impeller_canvas_test_toImage.png | Bin 427 -> 0 bytes 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 testing/resources/impeller_canvas_test_toImage.png diff --git a/shell/testing/tester_main.cc b/shell/testing/tester_main.cc index 72a8d1d8505ff..9c0059b612160 100644 --- a/shell/testing/tester_main.cc +++ b/shell/testing/tester_main.cc @@ -196,7 +196,7 @@ class TesterPlatformView : public PlatformView, private: sk_sp sk_surface_ = nullptr; - ImpellerVulkanContextHolder impeller_context_holder_; + [[maybe_unused]] ImpellerVulkanContextHolder impeller_context_holder_; std::shared_ptr external_view_embedder_ = std::make_shared(); }; diff --git a/testing/dart/canvas_test.dart b/testing/dart/canvas_test.dart index 3a38fb62d7ed8..d317e2f13e420 100644 --- a/testing/dart/canvas_test.dart +++ b/testing/dart/canvas_test.dart @@ -192,9 +192,9 @@ void main() { expect(image.height, equals(100)); final bool areEqual = - await fuzzyGoldenImageCompare(image, '${impellerEnabled ? 'impeller_' : ''}canvas_test_toImage.png'); + await fuzzyGoldenImageCompare(image, 'canvas_test_toImage.png'); expect(areEqual, true); - }); + }, skip: impellerEnabled); Gradient makeGradient() { return Gradient.linear( diff --git a/testing/resources/impeller_canvas_test_toImage.png b/testing/resources/impeller_canvas_test_toImage.png deleted file mode 100644 index 3c1c0f0bed47fb48bd77d1f08f08fdba165f3a3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^DImEakt zG3V{=z`Q005!MTt=3&#_%9m+xmY;eg!clNu%qh#0Ek^{5iqa>TJ7oh60D%Lx^4aAN zPCeZ8cAeetT{-^K-+nx2r?Vz|zxpqmjk2>7#IA0AsmNn`@XUr3fu@-&jd&LNeCac2 z`Lv|GO>qkQH4bMD=4(8Q^J{B2b6#zIxbw%4^IlpF?zLia)-Q`DUYQ;^Rarf??2DP9 z_o_6HM;&*r@KMn>7di+UMym>Fp zV^3H0=Pkb;hn3hSwY99|c=DxU`c;+hYqzGg9l05;@bT)dLj~-7xqnqBE!-9@CLSO1 zReI8or^3I#wA-&Z6ml=MR$Q)n)slTZpC&lZcx9!Mn4xkMej(*Nb3%l&;~wR_L2U0VhTAvn3<5mdKI;Vst0G#`{-2eap From 48a6d5250fe95674744f9d9d60ee2f5cd03137ac Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 11 Oct 2023 13:08:14 -0700 Subject: [PATCH 20/22] Fix analysis issues --- testing/dart/fragment_shader_test.dart | 4 +--- testing/dart/gpu_test.dart | 13 +++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/testing/dart/fragment_shader_test.dart b/testing/dart/fragment_shader_test.dart index eef157267f174..b1f8821423643 100644 --- a/testing/dart/fragment_shader_test.dart +++ b/testing/dart/fragment_shader_test.dart @@ -5,16 +5,14 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert' as convert; -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; import 'package:path/path.dart' as path; -import 'shader_test_file_utils.dart'; - import 'impeller_enabled.dart'; +import 'shader_test_file_utils.dart'; void main() async { bool assertsEnabled = false; diff --git a/testing/dart/gpu_test.dart b/testing/dart/gpu_test.dart index fc3c0c5fff951..9d65c8ed4a66e 100644 --- a/testing/dart/gpu_test.dart +++ b/testing/dart/gpu_test.dart @@ -25,15 +25,16 @@ void main() { }); test('gpu.context throws exception for incompatible embedders', () async { - if (impellerEnabled) { - expect(gpu.gpuContext != null, true); - return; - } try { // ignore: unnecessary_statements - gpu.gpuContext; // Force the - fail('Exception not thrown'); + gpu.gpuContext; // Force the context to instantiate. + if (!impellerEnabled) { + fail('Exception not thrown, but no Impeller context available.'); + } } catch (e) { + if (impellerEnabled) { + fail('Exception thrown even though Impeller is enabled.'); + } expect( e.toString(), contains( From 46a393a03e0a38c298b0aa96618551e9c337430c Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 11 Oct 2023 13:37:11 -0700 Subject: [PATCH 21/22] oops --- testing/dart/fragment_shader_test.dart | 1 + testing/dart/observatory/vmservice_methods_test.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/dart/fragment_shader_test.dart b/testing/dart/fragment_shader_test.dart index b1f8821423643..da34695df7814 100644 --- a/testing/dart/fragment_shader_test.dart +++ b/testing/dart/fragment_shader_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert' as convert; +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; diff --git a/testing/dart/observatory/vmservice_methods_test.dart b/testing/dart/observatory/vmservice_methods_test.dart index 717e7fbed7e31..14fd20d2afcb1 100644 --- a/testing/dart/observatory/vmservice_methods_test.dart +++ b/testing/dart/observatory/vmservice_methods_test.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; From 91804dc2c3f1c6a56a2e2e5efdd036cc4691df57 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 11 Oct 2023 14:10:47 -0700 Subject: [PATCH 22/22] pylint --- testing/run_tests.py | 120 ++++++++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 48 deletions(-) diff --git a/testing/run_tests.py b/testing/run_tests.py index 3ba6f1f424a57..500dbba643d03 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -576,14 +576,37 @@ def run_engine_benchmarks(build_dir, executable_filter): ) -def gather_dart_test( - build_dir, - dart_file, - multithreaded, - enable_impeller=False, - enable_observatory=False, - expect_failure=False, -): +class FlutterTesterOptions(): + + def __init__( + self, + multithreaded=False, + enable_impeller=False, + enable_observatory=False, + expect_failure=False + ): + self.multithreaded = multithreaded + self.enable_impeller = enable_impeller + self.enable_observatory = enable_observatory + self.expect_failure = expect_failure + + def apply_args(self, command_args): + if not self.enable_observatory: + command_args.append('--disable-observatory') + + if self.enable_impeller: + command_args += ['--enable-impeller'] + + if self.multithreaded: + command_args.insert(0, '--force-multithreading') + + def threading_description(self): + if self.multithreaded: + return 'multithreaded' + return 'single-threaded' + + +def gather_dart_test(build_dir, dart_file, options): kernel_file_name = os.path.basename(dart_file) + '.dill' kernel_file_output = os.path.join(build_dir, 'gen', kernel_file_name) error_message = "%s doesn't exist. Please run the build that populates %s" % ( @@ -592,8 +615,8 @@ def gather_dart_test( assert os.path.isfile(kernel_file_output), error_message command_args = [] - if not enable_observatory: - command_args.append('--disable-observatory') + + options.apply_args(command_args) dart_file_contents = open(dart_file, 'r') custom_options = re.findall( @@ -602,9 +625,6 @@ def gather_dart_test( dart_file_contents.close() command_args.extend(custom_options) - if (enable_impeller): - command_args += ['--enable-impeller'] - command_args += [ '--use-test-fonts', '--icu-data-file-path=%s' % os.path.join(build_dir, 'icudtl.dat'), @@ -614,18 +634,12 @@ def gather_dart_test( kernel_file_output, ] - if multithreaded: - threading = 'multithreaded' - command_args.insert(0, '--force-multithreading') - else: - threading = 'single-threaded' - tester_name = 'flutter_tester' logger.info( "Running test '%s' using '%s' (%s)", kernel_file_name, tester_name, - threading + options.threading_description() ) - forbidden_output = [] if 'unopt' in build_dir or expect_failure else [ + forbidden_output = [] if 'unopt' in build_dir or options.expect_failure else [ '[ERROR' ] return EngineExecutableTask( @@ -634,7 +648,7 @@ def gather_dart_test( None, command_args, forbidden_output=forbidden_output, - expect_failure=expect_failure, + expect_failure=options.expect_failure, ) @@ -854,32 +868,36 @@ def gather_dart_tests(build_dir, test_filter): "Gathering dart test '%s' with observatory enabled", dart_test_file ) yield gather_dart_test( - build_dir, - dart_test_file, - multithreaded=True, - enable_impeller=False, - enable_observatory=True, + build_dir, dart_test_file, + FlutterTesterOptions( + multithreaded=True, + enable_impeller=False, + enable_observatory=True + ) ) yield gather_dart_test( - build_dir, - dart_test_file, - multithreaded=True, - enable_impeller=True, - enable_observatory=True + build_dir, dart_test_file, + FlutterTesterOptions( + multithreaded=True, + enable_impeller=True, + enable_observatory=True + ) ) yield gather_dart_test( - build_dir, - dart_test_file, - multithreaded=False, - enable_impeller=False, - enable_observatory=True + build_dir, dart_test_file, + FlutterTesterOptions( + multithreaded=False, + enable_impeller=False, + enable_observatory=True + ) ) yield gather_dart_test( - build_dir, - dart_test_file, - multithreaded=False, - enable_impeller=True, - enable_observatory=True + build_dir, dart_test_file, + FlutterTesterOptions( + multithreaded=False, + enable_impeller=True, + enable_observatory=True + ) ) for dart_test_file in dart_tests: @@ -889,16 +907,20 @@ def gather_dart_tests(build_dir, test_filter): else: logger.info("Gathering dart test '%s'", dart_test_file) yield gather_dart_test( - build_dir, dart_test_file, multithreaded=True, enable_impeller=False + build_dir, dart_test_file, + FlutterTesterOptions(multithreaded=True, enable_impeller=False) ) yield gather_dart_test( - build_dir, dart_test_file, multithreaded=True, enable_impeller=True + build_dir, dart_test_file, + FlutterTesterOptions(multithreaded=True, enable_impeller=True) ) yield gather_dart_test( - build_dir, dart_test_file, multithreaded=False, enable_impeller=False + build_dir, dart_test_file, + FlutterTesterOptions(multithreaded=False, enable_impeller=False) ) yield gather_dart_test( - build_dir, dart_test_file, multithreaded=False, enable_impeller=True + build_dir, dart_test_file, + FlutterTesterOptions(multithreaded=False, enable_impeller=True) ) @@ -915,10 +937,12 @@ def gather_dart_smoke_test(build_dir, test_filter): logger.info("Skipping '%s' due to filter.", smoke_test) else: yield gather_dart_test( - build_dir, smoke_test, multithreaded=True, expect_failure=True + build_dir, smoke_test, + FlutterTesterOptions(multithreaded=True, expect_failure=True) ) yield gather_dart_test( - build_dir, smoke_test, multithreaded=False, expect_failure=True + build_dir, smoke_test, + FlutterTesterOptions(multithreaded=False, expect_failure=True) )