Flutter Impeller
golden_playground_test_mac.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <dlfcn.h>
6 #include <filesystem>
7 #include <memory>
8 
9 #include "display_list/display_list.h"
11 
15 #include "fml/closure.h"
25 #include "third_party/abseil-cpp/absl/base/no_destructor.h"
26 
27 #define GLFW_INCLUDE_NONE
28 #include "third_party/glfw/include/GLFW/glfw3.h"
29 
30 namespace impeller {
31 
32 namespace {
33 std::unique_ptr<PlaygroundImpl> MakeVulkanPlayground(bool enable_validations) {
34  FML_CHECK(::glfwInit() == GLFW_TRUE);
35  PlaygroundSwitches playground_switches;
36  playground_switches.enable_vulkan_validation = enable_validations;
38  playground_switches);
39 }
40 
41 // Returns a static instance to a playground that can be used across tests.
42 const std::unique_ptr<PlaygroundImpl>& GetSharedVulkanPlayground(
43  bool enable_validations) {
44  if (enable_validations) {
45  static absl::NoDestructor<std::unique_ptr<PlaygroundImpl>>
46  vulkan_validation_playground(
47  MakeVulkanPlayground(/*enable_validations=*/true));
48  // TODO(142237): This can be removed when the thread local storage is
49  // removed.
50  static fml::ScopedCleanupClosure context_cleanup(
51  [&] { (*vulkan_validation_playground)->GetContext()->Shutdown(); });
52  return *vulkan_validation_playground;
53  } else {
54  static absl::NoDestructor<std::unique_ptr<PlaygroundImpl>>
55  vulkan_playground(MakeVulkanPlayground(/*enable_validations=*/false));
56  // TODO(142237): This can be removed when the thread local storage is
57  // removed.
58  static fml::ScopedCleanupClosure context_cleanup(
59  [&] { (*vulkan_playground)->GetContext()->Shutdown(); });
60  return *vulkan_playground;
61  }
62 }
63 
64 std::unique_ptr<PlaygroundImpl> MakeOpenGLESPlayground(bool use_sdfs = false) {
65  FML_CHECK(::glfwInit() == GLFW_TRUE);
66  PlaygroundSwitches playground_switches;
67  playground_switches.use_angle = true;
68  playground_switches.flags.use_sdfs = use_sdfs;
71  playground_switches);
72 }
73 
74 // Returns a static instance to an OpenGL ES playground that can be used across
75 // tests.
76 const std::unique_ptr<PlaygroundImpl>& GetSharedOpenGLESPlayground(
77  bool use_sdfs) {
78  if (use_sdfs) {
79  static absl::NoDestructor<std::unique_ptr<PlaygroundImpl>>
80  opengl_playground(MakeOpenGLESPlayground(/*use_sdfs=*/true));
81  static fml::ScopedCleanupClosure context_cleanup(
82  [&] { (*opengl_playground)->GetContext()->Shutdown(); });
83  return *opengl_playground;
84  } else {
85  static absl::NoDestructor<std::unique_ptr<PlaygroundImpl>>
86  opengl_playground(MakeOpenGLESPlayground(/*use_sdfs=*/false));
87  // TODO(142237): This can be removed when the thread local storage is
88  // removed.
89  static fml::ScopedCleanupClosure context_cleanup(
90  [&] { (*opengl_playground)->GetContext()->Shutdown(); });
91  return *opengl_playground;
92  }
93 }
94 
95 } // namespace
96 
97 #define IMP_AIKSTEST(name) \
98  "impeller_Play_AiksTest_" #name "_Metal", \
99  "impeller_Play_AiksTest_" #name "_OpenGLES", \
100  "impeller_Play_AiksTest_" #name "_Vulkan"
101 
102 // If you add a new playground test to the aiks unittests and you do not want it
103 // to also be a golden test, then add the test name here.
104 static const std::vector<std::string> kSkipTests = {
105  // TextRotated is flakey and we can't seem to get it to stabilize on Skia
106  // Gold.
107  IMP_AIKSTEST(TextRotated),
108  // Runtime stage based tests get confused with a Metal context.
109  "impeller_Play_AiksTest_CanRenderClippedRuntimeEffects_Vulkan",
110 };
111 
112 namespace {
113 std::string GetTestName() {
114  std::string suite_name =
115  ::testing::UnitTest::GetInstance()->current_test_suite()->name();
116  std::string test_name =
117  ::testing::UnitTest::GetInstance()->current_test_info()->name();
118  std::stringstream ss;
119  ss << "impeller_" << suite_name << "_" << test_name;
120  std::string result = ss.str();
121  // Make sure there are no slashes in the test name.
122  std::replace(result.begin(), result.end(), '/', '_');
123  return result;
124 }
125 
126 std::string GetGoldenFilename(const std::string& postfix) {
127  return GetTestName() + postfix + ".png";
128 }
129 } // namespace
130 
132  std::unique_ptr<testing::Screenshot> screenshot,
133  const std::string& postfix) {
134  if (!screenshot || !screenshot->GetBytes()) {
135  FML_LOG(ERROR) << "Failed to collect screenshot for test " << GetTestName();
136  return false;
137  }
138  std::string test_name = GetTestName();
139  std::string filename = GetGoldenFilename(postfix);
141  test_name, filename, screenshot->GetWidth(), screenshot->GetHeight());
142  if (!screenshot->WriteToPNG(
144  FML_LOG(ERROR) << "Failed to write screenshot to " << filename;
145  return false;
146  }
147  return true;
148 }
149 
151  std::unique_ptr<PlaygroundImpl> test_vulkan_playground;
152  std::unique_ptr<PlaygroundImpl> test_opengl_playground;
153  std::unique_ptr<testing::Screenshotter> screenshotter;
154  ISize window_size = ISize{1024, 768};
155 };
156 
158  : typographer_context_(TypographerContextSkia::Make()),
160 
162 
164  std::shared_ptr<TypographerContext> typographer_context) {
165  typographer_context_ = std::move(typographer_context);
166 };
167 
169  ASSERT_FALSE(dlopen("/usr/local/lib/libMoltenVK.dylib", RTLD_NOLOAD));
170 
171  auto context = GetContext();
172  if (context) {
173  context->DisposeThreadLocalCachedResources();
174  }
175 }
176 
177 namespace {
178 bool DoesSupportWideGamutTests() {
179 #ifdef __arm64__
180  return true;
181 #else
182  return false;
183 #endif
184 }
185 } // namespace
186 
188  return DoesSupportWideGamutTests() && GetParam() == PlaygroundBackend::kMetal;
189 }
190 
192  std::filesystem::path testing_assets_path =
193  flutter::testing::GetTestingAssetsPath();
194  std::filesystem::path target_path = testing_assets_path.parent_path()
195  .parent_path()
196  .parent_path()
197  .parent_path();
198  std::filesystem::path icd_path = target_path / "vk_swiftshader_icd.json";
199  setenv("VK_ICD_FILENAMES", icd_path.c_str(), 1);
200 
201  std::string test_name = GetTestName();
202  PlaygroundSwitches switches;
203  switches.enable_wide_gamut =
204  test_name.find("WideGamut_") != std::string::npos;
205  switches.flags.antialiased_lines =
206  test_name.find("ExperimentAntialiasLines_") != std::string::npos;
207  switch (GetParam()) {
209  switches.flags.use_sdfs = true;
210  [[fallthrough]];
212  if (!DoesSupportWideGamutTests()) {
213  GTEST_SKIP()
214  << "This metal device doesn't support wide gamut golden tests.";
215  }
216  pimpl_->screenshotter =
217  std::make_unique<testing::MetalScreenshotter>(switches);
218  break;
220  if (switches.enable_wide_gamut) {
221  GTEST_SKIP() << "Vulkan doesn't support wide gamut golden tests.";
222  }
223  if (switches.flags.antialiased_lines) {
224  GTEST_SKIP()
225  << "Vulkan doesn't support antialiased lines golden tests.";
226  }
227  const std::unique_ptr<PlaygroundImpl>& playground =
228  GetSharedVulkanPlayground(/*enable_validations=*/true);
229  pimpl_->screenshotter =
230  std::make_unique<testing::VulkanScreenshotter>(playground);
231  break;
232  }
234  switches.flags.use_sdfs = true;
235  [[fallthrough]];
237  if (switches.enable_wide_gamut) {
238  GTEST_SKIP() << "OpenGLES doesn't support wide gamut golden tests.";
239  }
240  if (switches.flags.antialiased_lines) {
241  GTEST_SKIP()
242  << "OpenGLES doesn't support antialiased lines golden tests.";
243  }
244  const std::unique_ptr<PlaygroundImpl>& playground =
245  GetSharedOpenGLESPlayground(switches.flags.use_sdfs);
246  ::glfwMakeContextCurrent(
247  reinterpret_cast<GLFWwindow*>(playground->GetWindowHandle()));
248  pimpl_->screenshotter =
249  std::make_unique<testing::VulkanScreenshotter>(playground);
250  break;
251  }
252  }
253 
254  if (std::find(kSkipTests.begin(), kSkipTests.end(), test_name) !=
255  kSkipTests.end()) {
256  GTEST_SKIP()
257  << "GoldenPlaygroundTest doesn't support interactive playground tests "
258  "yet.";
259  }
260 
262  "gpu_string", GetContext()->DescribeGpuModel());
263 }
264 
266  return GetParam();
267 }
268 
270  const AiksDlPlaygroundCallback& callback) {
271  AiksContext renderer(GetContext(), typographer_context_);
272 
273  std::unique_ptr<testing::Screenshot> screenshot;
274  Point content_scale =
275  pimpl_->screenshotter->GetPlayground().GetContentScale();
276 
277  ISize physical_window_size(
278  std::round(pimpl_->window_size.width * content_scale.x),
279  std::round(pimpl_->window_size.height * content_scale.y));
280  for (int i = 0; i < 2; ++i) {
281  auto display_list = callback();
282  auto texture =
283  DisplayListToTexture(display_list, physical_window_size, renderer);
284  screenshot = pimpl_->screenshotter->MakeScreenshot(renderer, texture);
285  }
286  return SaveScreenshot(std::move(screenshot));
287 }
288 
290  const sk_sp<flutter::DisplayList>& list) {
291  return OpenPlaygroundHere([&list]() { return list; });
292 }
293 
295  const Playground::SinglePassCallback& callback) {
296  AiksContext renderer(GetContext(), typographer_context_);
297  std::shared_ptr<Context> context = GetContext();
298  Point content_scale =
299  pimpl_->screenshotter->GetPlayground().GetContentScale();
300  ISize size(std::round(pimpl_->window_size.width * content_scale.x),
301  std::round(pimpl_->window_size.height * content_scale.y));
302 
303  std::unique_ptr<testing::Screenshot> screenshot;
304  // Render twice so the second pass observes warmed pipeline and resource
305  // caches, matching the display list path above.
306  for (int i = 0; i < 2; ++i) {
307  RenderTargetAllocator render_target_allocator(
308  context->GetResourceAllocator());
309  RenderTarget render_target = render_target_allocator.CreateOffscreen(
310  *context, size, /*mip_count=*/1, "Golden Render Pass",
312  /*stencil_attachment_config=*/std::nullopt);
313  if (!render_target.IsValid()) {
314  return false;
315  }
316  std::shared_ptr<CommandBuffer> command_buffer =
317  context->CreateCommandBuffer();
318  if (!command_buffer) {
319  return false;
320  }
321  std::shared_ptr<RenderPass> render_pass =
322  command_buffer->CreateRenderPass(render_target);
323  if (!render_pass) {
324  return false;
325  }
326  if (!callback(*render_pass)) {
327  return false;
328  }
329  if (!render_pass->EncodeCommands()) {
330  return false;
331  }
332  if (!context->GetCommandQueue()->Submit({command_buffer}).ok()) {
333  return false;
334  }
335  screenshot = pimpl_->screenshotter->MakeScreenshot(
336  renderer, render_target.GetRenderTargetTexture());
337  }
338  return SaveScreenshot(std::move(screenshot));
339 }
340 
341 bool GoldenPlaygroundTest::ImGuiBegin(const char* name,
342  bool* p_open,
343  ImGuiWindowFlags flags) {
344  return false;
345 }
346 
348  const char* fixture_name,
349  bool enable_mipmapping) const {
350  std::shared_ptr<fml::Mapping> mapping =
351  flutter::testing::OpenFixtureAsMapping(fixture_name);
352  auto result = Playground::CreateTextureForMapping(GetContext(), mapping,
353  enable_mipmapping);
354  if (result) {
355  result->SetLabel(fixture_name);
356  }
357  return result;
358 }
359 
361  const char* fixture_name,
362  bool enable_mipmapping) const {
363  std::shared_ptr<Texture> texture =
364  CreateTextureForFixture(fixture_name, enable_mipmapping);
365  return DlImageImpeller::Make(texture);
366 }
367 
368 absl::StatusOr<RuntimeStage::Map> GoldenPlaygroundTest::OpenAssetAsRuntimeStage(
369  const char* asset_name) const {
370  const std::shared_ptr<fml::Mapping> fixture =
371  flutter::testing::OpenFixtureAsMapping(asset_name);
372  if (!fixture || fixture->GetSize() == 0) {
373  return absl::NotFoundError("Asset not found or empty.");
374  }
375  return RuntimeStage::DecodeRuntimeStages(fixture);
376 }
377 
378 std::shared_ptr<Context> GoldenPlaygroundTest::GetContext() const {
379  if (!pimpl_->screenshotter) {
380  return nullptr;
381  }
382  return pimpl_->screenshotter->GetPlayground().GetContext();
383 }
384 
385 std::shared_ptr<Context> GoldenPlaygroundTest::MakeContext() const {
386  if (GetParam() == PlaygroundBackend::kMetal ||
387  GetParam() == PlaygroundBackend::kMetalSDF) {
388  /// On Metal we create a context for each test.
389  return GetContext();
390  } else if (GetParam() == PlaygroundBackend::kVulkan) {
391  bool enable_vulkan_validations = true;
392  FML_CHECK(!pimpl_->test_vulkan_playground)
393  << "We don't support creating multiple contexts for one test";
394  pimpl_->test_vulkan_playground =
395  MakeVulkanPlayground(enable_vulkan_validations);
396  pimpl_->screenshotter = std::make_unique<testing::VulkanScreenshotter>(
397  pimpl_->test_vulkan_playground);
398  return pimpl_->test_vulkan_playground->GetContext();
399  } else if (GetParam() == PlaygroundBackend::kOpenGLES ||
400  GetParam() == PlaygroundBackend::kOpenGLESSDF) {
401  FML_CHECK(!pimpl_->test_opengl_playground)
402  << "We don't support creating multiple contexts for one test";
403  bool use_sdfs = (GetParam() == PlaygroundBackend::kOpenGLESSDF);
404  pimpl_->test_opengl_playground = MakeOpenGLESPlayground(use_sdfs);
405  pimpl_->screenshotter = std::make_unique<testing::VulkanScreenshotter>(
406  pimpl_->test_opengl_playground);
407  return pimpl_->test_opengl_playground->GetContext();
408  } else {
409  FML_CHECK(false);
410  return nullptr;
411  }
412 }
413 
415  return pimpl_->screenshotter->GetPlayground().GetContentScale();
416 }
417 
419  return 0.0f;
420 }
421 
423  return pimpl_->window_size;
424 }
425 
427  return IRect::MakeSize(pimpl_->window_size);
428 }
429 
430 void GoldenPlaygroundTest::GoldenPlaygroundTest::SetWindowSize(ISize size) {
431  pimpl_->window_size = size;
432 }
433 
435  const std::shared_ptr<Capabilities>& capabilities) {
436  return pimpl_->screenshotter->GetPlayground().SetCapabilities(capabilities);
437 }
438 
439 std::unique_ptr<testing::Screenshot> GoldenPlaygroundTest::MakeScreenshot(
440  const sk_sp<flutter::DisplayList>& list) {
441  AiksContext renderer(GetContext(), typographer_context_);
442  Point content_scale =
443  pimpl_->screenshotter->GetPlayground().GetContentScale();
444 
445  ISize physical_window_size(
446  std::round(pimpl_->window_size.width * content_scale.x),
447  std::round(pimpl_->window_size.height * content_scale.y));
448  return pimpl_->screenshotter->MakeScreenshot(
449  renderer, DisplayListToTexture(list, physical_window_size, renderer));
450 }
451 
453  return pimpl_->screenshotter->GetPlayground().GetRuntimeStageBackend();
454 }
455 
456 } // namespace impeller
static sk_sp< DlImageImpeller > Make(std::shared_ptr< Texture > texture, OwningContext owning_context=OwningContext::kIO)
RuntimeStageBackend GetRuntimeStageBackend() const
sk_sp< flutter::DlImage > CreateDlImageForFixture(const char *fixture_name, bool enable_mipmapping=false) const
fml::Status SetCapabilities(const std::shared_ptr< Capabilities > &capabilities)
void SetTypographerContext(std::shared_ptr< TypographerContext > typographer_context)
std::shared_ptr< Context > MakeContext() const
static bool SaveScreenshot(std::unique_ptr< testing::Screenshot > screenshot, const std::string &postfix="")
std::unique_ptr< testing::Screenshot > MakeScreenshot(const sk_sp< flutter::DisplayList > &list)
bool OpenPlaygroundHere(Picture picture)
static bool ImGuiBegin(const char *name, bool *p_open, ImGuiWindowFlags flags)
std::function< sk_sp< flutter::DisplayList >()> AiksDlPlaygroundCallback
absl::StatusOr< RuntimeStage::Map > OpenAssetAsRuntimeStage(const char *asset_name) const
std::shared_ptr< Context > GetContext() const
std::shared_ptr< Texture > CreateTextureForFixture(const char *fixture_name, bool enable_mipmapping=false) const
std::function< bool(RenderPass &pass)> SinglePassCallback
Definition: playground.h:39
static std::shared_ptr< Texture > CreateTextureForMapping(const std::shared_ptr< Context > &context, std::shared_ptr< fml::Mapping > mapping, bool enable_mipmapping=false)
Definition: playground.cc:522
static std::unique_ptr< PlaygroundImpl > Create(PlaygroundBackend backend, PlaygroundSwitches switches)
a wrapper around the impeller [Allocator] instance that can be used to provide caching of allocated r...
virtual RenderTarget CreateOffscreen(const Context &context, ISize size, int mip_count, std::string_view label="Offscreen", RenderTarget::AttachmentConfig color_attachment_config=RenderTarget::kDefaultColorAttachmentConfig, std::optional< RenderTarget::AttachmentConfig > stencil_attachment_config=RenderTarget::kDefaultStencilAttachmentConfig, const std::shared_ptr< Texture > &existing_color_texture=nullptr, const std::shared_ptr< Texture > &existing_depth_stencil_texture=nullptr, std::optional< PixelFormat > target_pixel_format=std::nullopt)
std::shared_ptr< Texture > GetRenderTargetTexture() const
static constexpr AttachmentConfig kDefaultColorAttachmentConfig
Definition: render_target.h:55
static absl::StatusOr< Map > DecodeRuntimeStages(const std::shared_ptr< fml::Mapping > &payload)
static GoldenDigest * Instance()
void AddDimension(const std::string &name, const std::string &value)
void AddImage(const std::string &test_name, const std::string &filename, int32_t width, int32_t height)
std::string GetFilenamePath(const std::string &filename) const
static WorkingDirectory * Instance()
#define IMP_AIKSTEST(name)
static const std::vector< std::string > kSkipTests
float Scalar
Definition: scalar.h:19
std::shared_ptr< Texture > DisplayListToTexture(const sk_sp< flutter::DisplayList > &display_list, ISize size, AiksContext &context, bool reset_host_buffer, bool generate_mips, std::optional< PixelFormat > target_pixel_format)
Render the provided display list to a texture with the given size.
PlaygroundBackend
Definition: playground.h:27
std::shared_ptr< ContextGLES > context
std::shared_ptr< RenderPass > render_pass
std::shared_ptr< CommandBuffer > command_buffer
bool antialiased_lines
When turned on DrawLine will use the experimental antialiased path.
Definition: flags.h:11
bool use_sdfs
Use SDFs for rendering.
Definition: flags.h:13
constexpr static TRect MakeSize(const TSize< U > &size)
Definition: rect.h:150