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