Flutter Impeller
playground.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 <array>
6 #include <memory>
7 #include <optional>
8 #include <sstream>
9 
10 #include "fml/closure.h"
11 #include "fml/time/time_point.h"
18 
19 #define GLFW_INCLUDE_NONE
20 #include "third_party/glfw/include/GLFW/glfw3.h"
21 
22 #include "flutter/fml/paths.h"
23 #include "flutter/testing/test_swiftshader_utils.h"
26 #include "impeller/core/formats.h"
34 #include "third_party/imgui/backends/imgui_impl_glfw.h"
35 #include "third_party/imgui/imgui.h"
36 
37 #if FML_OS_MACOSX
38 #include "fml/platform/darwin/scoped_nsautorelease_pool.h"
39 #endif // FML_OS_MACOSX
40 
41 #if IMPELLER_ENABLE_VULKAN
43 #endif // IMPELLER_ENABLE_VULKAN
44 
45 namespace impeller {
46 
48  switch (backend) {
50  return "Metal";
52  return "MetalSDF";
54  return "OpenGLES";
56  return "Vulkan";
57  }
58  FML_UNREACHABLE();
59 }
60 
61 static void InitializeGLFWOnce() {
62  // This guard is a hack to work around a problem where glfwCreateWindow
63  // hangs when opening a second window after GLFW has been reinitialized (for
64  // example, when flipping through multiple playground tests).
65  //
66  // Explanation:
67  // * glfwCreateWindow calls [NSApp run], which begins running the event
68  // loop on the current thread.
69  // * GLFW then immediately stops the loop when
70  // applicationDidFinishLaunching is fired.
71  // * applicationDidFinishLaunching is only ever fired once during the
72  // application's lifetime, so subsequent calls to [NSApp run] will always
73  // hang with this setup.
74  // * glfwInit resets the flag that guards against [NSApp run] being
75  // called a second time, which causes the subsequent `glfwCreateWindow`
76  // to hang indefinitely in the event loop, because
77  // applicationDidFinishLaunching is never fired.
78  static std::once_flag sOnceInitializer;
79  std::call_once(sOnceInitializer, []() {
80  ::glfwSetErrorCallback([](int code, const char* description) {
81  FML_LOG(ERROR) << "GLFW Error '" << description << "' (" << code << ").";
82  });
83  FML_CHECK(::glfwInit() == GLFW_TRUE);
84  });
85 }
86 
87 Playground::Playground(PlaygroundSwitches switches) : switches_(switches) {
89  flutter::testing::SetupSwiftshaderOnce(switches_.use_swiftshader);
90 }
91 
92 Playground::~Playground() = default;
93 
94 std::shared_ptr<Context> Playground::GetContext() const {
95  return context_;
96 }
97 
98 std::shared_ptr<Context> Playground::MakeContext() const {
99  // Playgrounds are already making a context for each test, so we can just
100  // return the `context_`.
101  return context_;
102 }
103 
105  switch (backend) {
108 #if IMPELLER_ENABLE_METAL
109  return true;
110 #else // IMPELLER_ENABLE_METAL
111  return false;
112 #endif // IMPELLER_ENABLE_METAL
114 #if IMPELLER_ENABLE_OPENGLES
115  return true;
116 #else // IMPELLER_ENABLE_OPENGLES
117  return false;
118 #endif // IMPELLER_ENABLE_OPENGLES
120 #if IMPELLER_ENABLE_VULKAN
122 #else // IMPELLER_ENABLE_VULKAN
123  return false;
124 #endif // IMPELLER_ENABLE_VULKAN
125  }
126  FML_UNREACHABLE();
127 }
128 
130  const PlaygroundSwitches& switches) {
131  FML_CHECK(SupportsBackend(backend));
132 
133  impl_ = PlaygroundImpl::Create(backend, switches);
134  if (!impl_) {
135  FML_LOG(WARNING) << "PlaygroundImpl::Create failed.";
136  return;
137  }
138 
139  context_ = impl_->GetContext();
140 }
141 
143  if (!context_) {
144  FML_LOG(WARNING) << "Asked to set up a window with no context (call "
145  "SetupContext first).";
146  return;
147  }
148  start_time_ = fml::TimePoint::Now().ToEpochDelta();
149 }
150 
153 }
154 
156  if (host_buffer_) {
157  host_buffer_.reset();
158  }
159  if (context_) {
160  context_->Shutdown();
161  }
162  context_.reset();
163  impl_.reset();
164 }
165 
166 static std::atomic_bool gShouldOpenNewPlaygrounds = true;
167 
170 }
171 
172 static void PlaygroundKeyCallback(GLFWwindow* window,
173  int key,
174  int scancode,
175  int action,
176  int mods) {
177  if ((key == GLFW_KEY_ESCAPE) && action == GLFW_RELEASE) {
178  if (mods & (GLFW_MOD_CONTROL | GLFW_MOD_SUPER | GLFW_MOD_SHIFT)) {
180  }
181  ::glfwSetWindowShouldClose(window, GLFW_TRUE);
182  }
183 }
184 
186  return cursor_position_;
187 }
188 
190  return window_size_;
191 }
192 
194  return impl_->GetContentScale();
195 }
196 
198  return (fml::TimePoint::Now().ToEpochDelta() - start_time_).ToSecondsF();
199 }
200 
201 void Playground::SetCursorPosition(Point pos) {
202  cursor_position_ = pos;
203 }
204 
206  const Playground::RenderCallback& render_callback) {
208  return true;
209  }
210 
211  if (!render_callback) {
212  return true;
213  }
214 
215  IMGUI_CHECKVERSION();
217  fml::ScopedCleanupClosure destroy_imgui_context(
218  []() { ImGui::DestroyContext(); });
219  ImGui::StyleColorsDark();
220 
221  auto& io = ImGui::GetIO();
222  io.IniFilename = nullptr;
223  io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
224  io.ConfigWindowsResizeFromEdges = true;
225 
226  auto window = reinterpret_cast<GLFWwindow*>(impl_->GetWindowHandle());
227  if (!window) {
228  return false;
229  }
230  ::glfwSetWindowTitle(window, GetWindowTitle().c_str());
231  ::glfwSetWindowUserPointer(window, this);
232  ::glfwSetWindowSizeCallback(
233  window, [](GLFWwindow* window, int width, int height) -> void {
234  auto playground =
235  reinterpret_cast<Playground*>(::glfwGetWindowUserPointer(window));
236  if (!playground) {
237  return;
238  }
239  playground->SetWindowSize(ISize{width, height}.Max({}));
240  });
241  ::glfwSetKeyCallback(window, &PlaygroundKeyCallback);
242  ::glfwSetCursorPosCallback(window, [](GLFWwindow* window, double x,
243  double y) {
244  reinterpret_cast<Playground*>(::glfwGetWindowUserPointer(window))
245  ->SetCursorPosition({static_cast<Scalar>(x), static_cast<Scalar>(y)});
246  });
247 
248  ImGui_ImplGlfw_InitForOther(window, true);
249  fml::ScopedCleanupClosure shutdown_imgui([]() { ImGui_ImplGlfw_Shutdown(); });
250 
251  ImGui_ImplImpeller_Init(context_);
252  fml::ScopedCleanupClosure shutdown_imgui_impeller(
253  []() { ImGui_ImplImpeller_Shutdown(); });
254 
255  ImGui::SetNextWindowPos({10, 10});
256 
257  ::glfwSetWindowSize(window, GetWindowSize().width, GetWindowSize().height);
258  ::glfwSetWindowPos(window, 200, 100);
259  ::glfwShowWindow(window);
260 
261  while (true) {
262 #if FML_OS_MACOSX
263  fml::ScopedNSAutoreleasePool pool;
264 #endif
265  ::glfwPollEvents();
266 
267  if (::glfwWindowShouldClose(window)) {
268  return true;
269  }
270 
271  ImGui_ImplGlfw_NewFrame();
272 
273  auto surface = impl_->AcquireSurfaceFrame(context_);
274  RenderTarget render_target = surface->GetRenderTarget();
275 
276  ImGui::NewFrame();
277  ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(),
278  ImGuiDockNodeFlags_PassthruCentralNode);
279  bool result = render_callback(render_target);
280  ImGui::Render();
281 
282  // Render ImGui overlay.
283  {
284  auto buffer = context_->CreateCommandBuffer();
285  if (!buffer) {
286  VALIDATION_LOG << "Could not create command buffer.";
287  return false;
288  }
289  buffer->SetLabel("ImGui Command Buffer");
290 
291  auto color0 = render_target.GetColorAttachment(0);
293  if (color0.resolve_texture) {
294  color0.texture = color0.resolve_texture;
295  color0.resolve_texture = nullptr;
296  color0.store_action = StoreAction::kStore;
297  }
298  render_target.SetColorAttachment(color0, 0);
299  render_target.SetStencilAttachment(std::nullopt);
300  render_target.SetDepthAttachment(std::nullopt);
301 
302  auto pass = buffer->CreateRenderPass(render_target);
303  if (!pass) {
304  VALIDATION_LOG << "Could not create render pass.";
305  return false;
306  }
307  pass->SetLabel("ImGui Render Pass");
308  if (!host_buffer_) {
309  host_buffer_ = HostBuffer::Create(
310  context_->GetResourceAllocator(), context_->GetIdleWaiter(),
311  context_->GetCapabilities()->GetMinimumUniformAlignment());
312  }
313 
314  ImGui_ImplImpeller_RenderDrawData(ImGui::GetDrawData(), *pass,
315  *host_buffer_);
316 
317  pass->EncodeCommands();
318 
319  if (!context_->GetCommandQueue()->Submit({buffer}).ok()) {
320  return false;
321  }
322  }
323 
324  if (!result || !surface->Present()) {
325  return false;
326  }
327 
328  if (!ShouldKeepRendering()) {
329  break;
330  }
331  }
332 
333  ::glfwHideWindow(window);
334 
335  return true;
336 }
337 
339  return OpenPlaygroundHere(
340  [context = GetContext(), &pass_callback](RenderTarget& render_target) {
341  auto buffer = context->CreateCommandBuffer();
342  if (!buffer) {
343  return false;
344  }
345  buffer->SetLabel("Playground Command Buffer");
346 
347  auto pass = buffer->CreateRenderPass(render_target);
348  if (!pass) {
349  return false;
350  }
351  pass->SetLabel("Playground Render Pass");
352 
353  if (!pass_callback(*pass)) {
354  return false;
355  }
356 
357  pass->EncodeCommands();
358  if (!context->GetCommandQueue()->Submit({buffer}).ok()) {
359  return false;
360  }
361  return true;
362  });
363 }
364 
365 std::shared_ptr<CompressedImage> Playground::LoadFixtureImageCompressed(
366  std::shared_ptr<fml::Mapping> mapping) {
367  auto compressed_image = CompressedImageSkia::Create(std::move(mapping));
368  if (!compressed_image) {
369  VALIDATION_LOG << "Could not create compressed image.";
370  return nullptr;
371  }
372 
373  return compressed_image;
374 }
375 
376 std::optional<DecompressedImage> Playground::DecodeImageRGBA(
377  const std::shared_ptr<CompressedImage>& compressed) {
378  if (compressed == nullptr) {
379  return std::nullopt;
380  }
381  // The decoded image is immediately converted into RGBA as that format is
382  // known to be supported everywhere. For image sources that don't need 32
383  // bit pixel strides, this is overkill. Since this is a test fixture we
384  // aren't necessarily trying to eke out memory savings here and instead
385  // favor simplicity.
386  auto image = compressed->Decode().ConvertToRGBA();
387  if (!image.IsValid()) {
388  VALIDATION_LOG << "Could not decode image.";
389  return std::nullopt;
390  }
391 
392  return image;
393 }
394 
395 static std::shared_ptr<Texture> CreateTextureForDecompressedImage(
396  const std::shared_ptr<Context>& context,
397  DecompressedImage& decompressed_image,
398  bool enable_mipmapping) {
399  TextureDescriptor texture_descriptor;
400  texture_descriptor.storage_mode = StorageMode::kDevicePrivate;
401  texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
402  texture_descriptor.size = decompressed_image.GetSize();
403  texture_descriptor.mip_count =
404  enable_mipmapping ? decompressed_image.GetSize().MipCount() : 1u;
405 
406  auto texture =
407  context->GetResourceAllocator()->CreateTexture(texture_descriptor);
408  if (!texture) {
409  VALIDATION_LOG << "Could not allocate texture for fixture.";
410  return nullptr;
411  }
412 
413  auto command_buffer = context->CreateCommandBuffer();
414  if (!command_buffer) {
415  FML_DLOG(ERROR) << "Could not create command buffer for mipmap generation.";
416  return nullptr;
417  }
418  command_buffer->SetLabel("Mipmap Command Buffer");
419 
420  auto blit_pass = command_buffer->CreateBlitPass();
421  auto buffer_view = DeviceBuffer::AsBufferView(
422  context->GetResourceAllocator()->CreateBufferWithCopy(
423  *decompressed_image.GetAllocation()));
424  blit_pass->AddCopy(buffer_view, texture);
425  if (enable_mipmapping) {
426  blit_pass->SetLabel("Mipmap Blit Pass");
427  blit_pass->GenerateMipmap(texture);
428  }
429  blit_pass->EncodeCommands();
430  if (!context->GetCommandQueue()->Submit({command_buffer}).ok()) {
431  FML_DLOG(ERROR) << "Failed to submit blit pass command buffer.";
432  return nullptr;
433  }
434  return texture;
435 }
436 
437 std::shared_ptr<Texture> Playground::CreateTextureForMapping(
438  const std::shared_ptr<Context>& context,
439  std::shared_ptr<fml::Mapping> mapping,
440  bool enable_mipmapping) {
441  auto image = Playground::DecodeImageRGBA(
442  Playground::LoadFixtureImageCompressed(std::move(mapping)));
443  if (!image.has_value()) {
444  return nullptr;
445  }
446  return CreateTextureForDecompressedImage(context, image.value(),
447  enable_mipmapping);
448 }
449 
450 std::shared_ptr<Texture> Playground::CreateTextureForFixture(
451  const char* fixture_name,
452  bool enable_mipmapping) const {
453  auto texture = CreateTextureForMapping(
454  context_, OpenAssetAsMapping(fixture_name), enable_mipmapping);
455  if (texture == nullptr) {
456  return nullptr;
457  }
458  texture->SetLabel(fixture_name);
459  return texture;
460 }
461 
463  std::array<const char*, 6> fixture_names) const {
464  std::array<DecompressedImage, 6> images;
465  for (size_t i = 0; i < fixture_names.size(); i++) {
466  auto image = DecodeImageRGBA(
468  if (!image.has_value()) {
469  return nullptr;
470  }
471  images[i] = image.value();
472  }
473 
474  TextureDescriptor texture_descriptor;
475  texture_descriptor.storage_mode = StorageMode::kDevicePrivate;
476  texture_descriptor.type = TextureType::kTextureCube;
477  texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
478  texture_descriptor.size = images[0].GetSize();
479  texture_descriptor.mip_count = 1u;
480 
481  auto texture =
482  context_->GetResourceAllocator()->CreateTexture(texture_descriptor);
483  if (!texture) {
484  VALIDATION_LOG << "Could not allocate texture cube.";
485  return nullptr;
486  }
487  texture->SetLabel("Texture cube");
488 
489  auto cmd_buffer = context_->CreateCommandBuffer();
490  auto blit_pass = cmd_buffer->CreateBlitPass();
491  for (size_t i = 0; i < fixture_names.size(); i++) {
492  auto device_buffer = context_->GetResourceAllocator()->CreateBufferWithCopy(
493  *images[i].GetAllocation());
494  blit_pass->AddCopy(DeviceBuffer::AsBufferView(device_buffer), texture, {},
495  "", /*mip_level=*/0, /*slice=*/i);
496  }
497 
498  if (!blit_pass->EncodeCommands() ||
499  !context_->GetCommandQueue()->Submit({std::move(cmd_buffer)}).ok()) {
500  VALIDATION_LOG << "Could not upload texture to device memory.";
501  return nullptr;
502  }
503 
504  return texture;
505 }
506 
508  window_size_ = size;
509 }
510 
512  return true;
513 }
514 
516  const std::shared_ptr<Capabilities>& capabilities) {
517  return impl_->SetCapabilities(capabilities);
518 }
519 
522 }
523 
525  const {
526  return impl_->CreateGLProcAddressResolver();
527 }
528 
530  const {
531  return impl_->CreateVKProcAddressResolver();
532 }
533 
535  impl_->SetGPUDisabled(value);
536 }
537 
539  return impl_->GetRuntimeStageBackend();
540 }
541 
542 } // namespace impeller
static std::shared_ptr< CompressedImage > Create(std::shared_ptr< const fml::Mapping > allocation)
const std::shared_ptr< const fml::Mapping > & GetAllocation() const
const ISize & GetSize() const
static BufferView AsBufferView(std::shared_ptr< DeviceBuffer > buffer)
Create a buffer view of this entire buffer.
static std::shared_ptr< HostBuffer > Create(const std::shared_ptr< Allocator > &allocator, const std::shared_ptr< const IdleWaiter > &idle_waiter, size_t minimum_uniform_alignment)
Definition: host_buffer.cc:21
Playground(PlaygroundSwitches switches)
Definition: playground.cc:87
bool OpenPlaygroundHere(const RenderCallback &render_callback)
Definition: playground.cc:205
std::shared_ptr< Context > MakeContext() const
Definition: playground.cc:98
bool IsPlaygroundEnabled() const
Definition: playground.cc:151
virtual bool ShouldKeepRendering() const
Definition: playground.cc:511
static bool ShouldOpenNewPlaygrounds()
Definition: playground.cc:168
Point GetCursorPosition() const
Definition: playground.cc:185
void SetWindowSize(ISize size)
Definition: playground.cc:507
static std::shared_ptr< CompressedImage > LoadFixtureImageCompressed(std::shared_ptr< fml::Mapping > mapping)
Definition: playground.cc:365
ISize GetWindowSize() const
Definition: playground.cc:189
std::function< bool(RenderPass &pass)> SinglePassCallback
Definition: playground.h:37
void SetupContext(PlaygroundBackend backend, const PlaygroundSwitches &switches)
Definition: playground.cc:129
GLProcAddressResolver CreateGLProcAddressResolver() const
Definition: playground.cc:524
bool WillRenderSomething() const
Returns true if OpenPlaygroundHere will actually render anything.
Definition: playground.cc:520
RuntimeStageBackend GetRuntimeStageBackend() const
Definition: playground.cc:538
virtual std::string GetWindowTitle() const =0
virtual std::unique_ptr< fml::Mapping > OpenAssetAsMapping(std::string asset_name) const =0
std::function< bool(RenderTarget &render_target)> RenderCallback
Definition: playground.h:68
void SetGPUDisabled(bool disabled) const
Mark the GPU as unavilable.
Definition: playground.cc:534
const PlaygroundSwitches switches_
Definition: playground.h:120
std::shared_ptr< Context > GetContext() const
Definition: playground.cc:94
static bool SupportsBackend(PlaygroundBackend backend)
Definition: playground.cc:104
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:437
Point GetContentScale() const
Definition: playground.cc:193
std::shared_ptr< Texture > CreateTextureForFixture(const char *fixture_name, bool enable_mipmapping=false) const
Definition: playground.cc:450
Scalar GetSecondsElapsed() const
Get the amount of time elapsed from the start of the playground's execution.
Definition: playground.cc:197
std::function< void *(void *instance, const char *proc_name)> VKProcAddressResolver
Definition: playground.h:109
std::function< void *(const char *proc_name)> GLProcAddressResolver
Definition: playground.h:105
static std::optional< DecompressedImage > DecodeImageRGBA(const std::shared_ptr< CompressedImage > &compressed)
Definition: playground.cc:376
std::shared_ptr< Texture > CreateTextureCubeForFixture(std::array< const char *, 6 > fixture_names) const
Definition: playground.cc:462
fml::Status SetCapabilities(const std::shared_ptr< Capabilities > &capabilities)
Definition: playground.cc:515
VKProcAddressResolver CreateVKProcAddressResolver() const
Definition: playground.cc:529
static std::unique_ptr< PlaygroundImpl > Create(PlaygroundBackend backend, PlaygroundSwitches switches)
ColorAttachment GetColorAttachment(size_t index) const
Get the color attachment at [index].
RenderTarget & SetColorAttachment(const ColorAttachment &attachment, size_t index)
RenderTarget & SetDepthAttachment(std::optional< DepthAttachment > attachment)
RenderTarget & SetStencilAttachment(std::optional< StencilAttachment > attachment)
int32_t value
int32_t x
void ImGui_ImplImpeller_RenderDrawData(ImDrawData *draw_data, impeller::RenderPass &render_pass, impeller::HostBuffer &host_buffer)
bool ImGui_ImplImpeller_Init(const std::shared_ptr< impeller::Context > &context)
void ImGui_ImplImpeller_Shutdown()
std::shared_ptr< ContextVK > CreateContext()
std::string PlaygroundBackendToString(PlaygroundBackend backend)
Definition: playground.cc:47
float Scalar
Definition: scalar.h:19
static void PlaygroundKeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods)
Definition: playground.cc:172
static void InitializeGLFWOnce()
Definition: playground.cc:61
static std::shared_ptr< Texture > CreateTextureForDecompressedImage(const std::shared_ptr< Context > &context, DecompressedImage &decompressed_image, bool enable_mipmapping)
Definition: playground.cc:395
static std::atomic_bool gShouldOpenNewPlaygrounds
Definition: playground.cc:166
PlaygroundBackend
Definition: playground.h:26
LoadAction load_action
Definition: formats.h:663
constexpr TSize Max(const TSize &o) const
Definition: size.h:97
constexpr size_t MipCount() const
Return the mip count of the texture.
Definition: size.h:137
A lightweight object that describes the attributes of a texture that can then used an allocator to cr...
#define VALIDATION_LOG
Definition: validation.h:91