blob: 4f61c5c5020ef4faaf43de2adf1a7223ad463e54 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <array>
#include <memory>
#include <optional>
#include <sstream>
#include "fml/time/time_point.h"
#include "impeller/image/backends/skia/compressed_image_skia.h"
#include "impeller/image/decompressed_image.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/runtime_stage/runtime_stage.h"
#define GLFW_INCLUDE_NONE
#include "third_party/glfw/include/GLFW/glfw3.h"
#include "flutter/fml/paths.h"
#include "impeller/base/validation.h"
#include "impeller/core/allocator.h"
#include "impeller/core/formats.h"
#include "impeller/image/compressed_image.h"
#include "impeller/playground/imgui/imgui_impl_impeller.h"
#include "impeller/playground/playground.h"
#include "impeller/playground/playground_impl.h"
#include "impeller/renderer/context.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/renderer/renderer.h"
#include "third_party/imgui/backends/imgui_impl_glfw.h"
#include "third_party/imgui/imgui.h"
#if FML_OS_MACOSX
#include <objc/message.h>
#include <objc/runtime.h>
#endif
namespace impeller {
std::string PlaygroundBackendToString(PlaygroundBackend backend) {
switch (backend) {
case PlaygroundBackend::kMetal:
return "Metal";
case PlaygroundBackend::kOpenGLES:
return "OpenGLES";
case PlaygroundBackend::kVulkan:
return "Vulkan";
}
FML_UNREACHABLE();
}
struct Playground::GLFWInitializer {
GLFWInitializer() {
// This guard is a hack to work around a problem where glfwCreateWindow
// hangs when opening a second window after GLFW has been reinitialized (for
// example, when flipping through multiple playground tests).
//
// Explanation:
// * glfwCreateWindow calls [NSApp run], which begins running the event
// loop on the current thread.
// * GLFW then immediately stops the loop when
// applicationDidFinishLaunching is fired.
// * applicationDidFinishLaunching is only ever fired once during the
// application's lifetime, so subsequent calls to [NSApp run] will always
// hang with this setup.
// * glfwInit resets the flag that guards against [NSApp run] being
// called a second time, which causes the subsequent `glfwCreateWindow`
// to hang indefinitely in the event loop, because
// applicationDidFinishLaunching is never fired.
static std::once_flag sOnceInitializer;
std::call_once(sOnceInitializer, []() {
::glfwSetErrorCallback([](int code, const char* description) {
FML_LOG(ERROR) << "GLFW Error '" << description << "' (" << code
<< ").";
});
FML_CHECK(::glfwInit() == GLFW_TRUE);
});
}
};
Playground::Playground(PlaygroundSwitches switches)
: switches_(switches),
glfw_initializer_(std::make_unique<GLFWInitializer>()) {}
Playground::~Playground() = default;
std::shared_ptr<Context> Playground::GetContext() const {
return context_;
}
bool Playground::SupportsBackend(PlaygroundBackend backend) {
switch (backend) {
case PlaygroundBackend::kMetal:
#if IMPELLER_ENABLE_METAL
return true;
#else // IMPELLER_ENABLE_METAL
return false;
#endif // IMPELLER_ENABLE_METAL
case PlaygroundBackend::kOpenGLES:
#if IMPELLER_ENABLE_OPENGLES
return true;
#else // IMPELLER_ENABLE_OPENGLES
return false;
#endif // IMPELLER_ENABLE_OPENGLES
case PlaygroundBackend::kVulkan:
#if IMPELLER_ENABLE_VULKAN
return true;
#else // IMPELLER_ENABLE_VULKAN
return false;
#endif // IMPELLER_ENABLE_VULKAN
}
FML_UNREACHABLE();
}
void Playground::SetupContext(PlaygroundBackend backend) {
FML_CHECK(SupportsBackend(backend));
impl_ = PlaygroundImpl::Create(backend, switches_);
if (!impl_) {
return;
}
context_ = impl_->GetContext();
}
void Playground::SetupWindow() {
if (!context_) {
FML_LOG(WARNING)
<< "Asked to setup a window with no context (call SetupContext first).";
return;
}
auto renderer = std::make_unique<Renderer>(context_);
if (!renderer->IsValid()) {
return;
}
renderer_ = std::move(renderer);
start_time_ = fml::TimePoint::Now().ToEpochDelta();
}
void Playground::TeardownWindow() {
context_.reset();
renderer_.reset();
impl_.reset();
}
static std::atomic_bool gShouldOpenNewPlaygrounds = true;
bool Playground::ShouldOpenNewPlaygrounds() {
return gShouldOpenNewPlaygrounds;
}
static void PlaygroundKeyCallback(GLFWwindow* window,
int key,
int scancode,
int action,
int mods) {
if ((key == GLFW_KEY_ESCAPE) && action == GLFW_RELEASE) {
if (mods & (GLFW_MOD_CONTROL | GLFW_MOD_SUPER | GLFW_MOD_SHIFT)) {
gShouldOpenNewPlaygrounds = false;
}
::glfwSetWindowShouldClose(window, GLFW_TRUE);
}
}
Point Playground::GetCursorPosition() const {
return cursor_position_;
}
ISize Playground::GetWindowSize() const {
return window_size_;
}
Point Playground::GetContentScale() const {
return impl_->GetContentScale();
}
Scalar Playground::GetSecondsElapsed() const {
return (fml::TimePoint::Now().ToEpochDelta() - start_time_).ToSecondsF();
}
void Playground::SetCursorPosition(Point pos) {
cursor_position_ = pos;
}
#if FML_OS_MACOSX
class AutoReleasePool {
public:
AutoReleasePool() {
pool_ = reinterpret_cast<msg_send>(objc_msgSend)(
objc_getClass("NSAutoreleasePool"), sel_getUid("new"));
}
~AutoReleasePool() {
reinterpret_cast<msg_send>(objc_msgSend)(pool_, sel_getUid("drain"));
}
private:
typedef id (*msg_send)(void*, SEL);
id pool_;
};
#endif
bool Playground::OpenPlaygroundHere(
const Renderer::RenderCallback& render_callback) {
if (!switches_.enable_playground) {
return true;
}
if (!render_callback) {
return true;
}
if (!renderer_ || !renderer_->IsValid()) {
return false;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
fml::ScopedCleanupClosure destroy_imgui_context(
[]() { ImGui::DestroyContext(); });
ImGui::StyleColorsDark();
auto& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigWindowsResizeFromEdges = true;
auto window = reinterpret_cast<GLFWwindow*>(impl_->GetWindowHandle());
if (!window) {
return false;
}
::glfwSetWindowTitle(window, GetWindowTitle().c_str());
::glfwSetWindowUserPointer(window, this);
::glfwSetWindowSizeCallback(
window, [](GLFWwindow* window, int width, int height) -> void {
auto playground =
reinterpret_cast<Playground*>(::glfwGetWindowUserPointer(window));
if (!playground) {
return;
}
playground->SetWindowSize(ISize{width, height}.Max({}));
});
::glfwSetKeyCallback(window, &PlaygroundKeyCallback);
::glfwSetCursorPosCallback(window, [](GLFWwindow* window, double x,
double y) {
reinterpret_cast<Playground*>(::glfwGetWindowUserPointer(window))
->SetCursorPosition({static_cast<Scalar>(x), static_cast<Scalar>(y)});
});
ImGui_ImplGlfw_InitForOther(window, true);
fml::ScopedCleanupClosure shutdown_imgui([]() { ImGui_ImplGlfw_Shutdown(); });
ImGui_ImplImpeller_Init(renderer_->GetContext());
fml::ScopedCleanupClosure shutdown_imgui_impeller(
[]() { ImGui_ImplImpeller_Shutdown(); });
ImGui::SetNextWindowPos({10, 10});
::glfwSetWindowSize(window, GetWindowSize().width, GetWindowSize().height);
::glfwSetWindowPos(window, 200, 100);
::glfwShowWindow(window);
while (true) {
#if FML_OS_MACOSX
AutoReleasePool pool;
#endif
::glfwPollEvents();
if (::glfwWindowShouldClose(window)) {
return true;
}
ImGui_ImplGlfw_NewFrame();
Renderer::RenderCallback wrapped_callback =
[render_callback,
&renderer = renderer_](RenderTarget& render_target) -> bool {
ImGui::NewFrame();
ImGui::DockSpaceOverViewport(ImGui::GetMainViewport(),
ImGuiDockNodeFlags_PassthruCentralNode);
bool result = render_callback(render_target);
ImGui::Render();
// Render ImGui overlay.
{
auto buffer = renderer->GetContext()->CreateCommandBuffer();
if (!buffer) {
return false;
}
buffer->SetLabel("ImGui Command Buffer");
if (render_target.GetColorAttachments().empty()) {
return false;
}
auto color0 = render_target.GetColorAttachments().find(0)->second;
color0.load_action = LoadAction::kLoad;
if (color0.resolve_texture) {
color0.texture = color0.resolve_texture;
color0.resolve_texture = nullptr;
color0.store_action = StoreAction::kStore;
}
render_target.SetColorAttachment(color0, 0);
render_target.SetStencilAttachment(std::nullopt);
render_target.SetDepthAttachment(std::nullopt);
auto pass = buffer->CreateRenderPass(render_target);
if (!pass) {
return false;
}
pass->SetLabel("ImGui Render Pass");
ImGui_ImplImpeller_RenderDrawData(ImGui::GetDrawData(), *pass);
pass->EncodeCommands();
if (!buffer->SubmitCommands()) {
return false;
}
}
return result;
};
if (!renderer_->Render(impl_->AcquireSurfaceFrame(renderer_->GetContext()),
wrapped_callback)) {
VALIDATION_LOG << "Could not render into the surface.";
return false;
}
if (!ShouldKeepRendering()) {
break;
}
}
::glfwHideWindow(window);
return true;
}
bool Playground::OpenPlaygroundHere(SinglePassCallback pass_callback) {
return OpenPlaygroundHere(
[context = GetContext(), &pass_callback](RenderTarget& render_target) {
auto buffer = context->CreateCommandBuffer();
if (!buffer) {
return false;
}
buffer->SetLabel("Playground Command Buffer");
auto pass = buffer->CreateRenderPass(render_target);
if (!pass) {
return false;
}
pass->SetLabel("Playground Render Pass");
if (!pass_callback(*pass)) {
return false;
}
pass->EncodeCommands();
if (!buffer->SubmitCommands()) {
return false;
}
return true;
});
}
std::shared_ptr<CompressedImage> Playground::LoadFixtureImageCompressed(
std::shared_ptr<fml::Mapping> mapping) {
auto compressed_image = CompressedImageSkia::Create(std::move(mapping));
if (!compressed_image) {
VALIDATION_LOG << "Could not create compressed image.";
return nullptr;
}
return compressed_image;
}
std::optional<DecompressedImage> Playground::DecodeImageRGBA(
const std::shared_ptr<CompressedImage>& compressed) {
if (compressed == nullptr) {
return std::nullopt;
}
// The decoded image is immediately converted into RGBA as that format is
// known to be supported everywhere. For image sources that don't need 32
// bit pixel strides, this is overkill. Since this is a test fixture we
// aren't necessarily trying to eke out memory savings here and instead
// favor simplicity.
auto image = compressed->Decode().ConvertToRGBA();
if (!image.IsValid()) {
VALIDATION_LOG << "Could not decode image.";
return std::nullopt;
}
return image;
}
static std::shared_ptr<Texture> CreateTextureForDecompressedImage(
const std::shared_ptr<Context>& context,
DecompressedImage& decompressed_image,
bool enable_mipmapping) {
// TODO(https://github.com/flutter/flutter/issues/123468): copying buffers to
// textures is not implemented for GLES/Vulkan.
if (context->GetCapabilities()->SupportsBufferToTextureBlits()) {
impeller::TextureDescriptor texture_descriptor;
texture_descriptor.storage_mode = impeller::StorageMode::kDevicePrivate;
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
texture_descriptor.size = decompressed_image.GetSize();
texture_descriptor.mip_count =
enable_mipmapping ? decompressed_image.GetSize().MipCount() : 1u;
auto dest_texture =
context->GetResourceAllocator()->CreateTexture(texture_descriptor);
if (!dest_texture) {
FML_DLOG(ERROR) << "Could not create Impeller texture.";
return nullptr;
}
auto buffer = context->GetResourceAllocator()->CreateBufferWithCopy(
*decompressed_image.GetAllocation().get());
dest_texture->SetLabel(
impeller::SPrintF("ui.Image(%p)", dest_texture.get()).c_str());
auto command_buffer = context->CreateCommandBuffer();
if (!command_buffer) {
FML_DLOG(ERROR)
<< "Could not create command buffer for mipmap generation.";
return nullptr;
}
command_buffer->SetLabel("Mipmap Command Buffer");
auto blit_pass = command_buffer->CreateBlitPass();
if (!blit_pass) {
FML_DLOG(ERROR) << "Could not create blit pass for mipmap generation.";
return nullptr;
}
blit_pass->SetLabel("Mipmap Blit Pass");
blit_pass->AddCopy(buffer->AsBufferView(), dest_texture);
if (enable_mipmapping) {
blit_pass->GenerateMipmap(dest_texture);
}
blit_pass->EncodeCommands(context->GetResourceAllocator());
if (!command_buffer->SubmitCommands()) {
FML_DLOG(ERROR) << "Failed to submit blit pass command buffer.";
return nullptr;
}
return dest_texture;
} else { // Doesn't support buffer-to-texture blits.
auto texture_descriptor = TextureDescriptor{};
texture_descriptor.storage_mode = StorageMode::kHostVisible;
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
texture_descriptor.size = decompressed_image.GetSize();
texture_descriptor.mip_count =
enable_mipmapping ? decompressed_image.GetSize().MipCount() : 1u;
auto texture =
context->GetResourceAllocator()->CreateTexture(texture_descriptor);
if (!texture) {
VALIDATION_LOG << "Could not allocate texture for fixture.";
return nullptr;
}
auto uploaded = texture->SetContents(decompressed_image.GetAllocation());
if (!uploaded) {
VALIDATION_LOG
<< "Could not upload texture to device memory for fixture.";
return nullptr;
}
return texture;
}
}
std::shared_ptr<Texture> Playground::CreateTextureForMapping(
const std::shared_ptr<Context>& context,
std::shared_ptr<fml::Mapping> mapping,
bool enable_mipmapping) {
auto image = Playground::DecodeImageRGBA(
Playground::LoadFixtureImageCompressed(std::move(mapping)));
if (!image.has_value()) {
return nullptr;
}
return CreateTextureForDecompressedImage(context, image.value(),
enable_mipmapping);
}
std::shared_ptr<Texture> Playground::CreateTextureForFixture(
const char* fixture_name,
bool enable_mipmapping) const {
auto texture = CreateTextureForMapping(renderer_->GetContext(),
OpenAssetAsMapping(fixture_name),
enable_mipmapping);
if (texture == nullptr) {
return nullptr;
}
texture->SetLabel(fixture_name);
return texture;
}
std::shared_ptr<Texture> Playground::CreateTextureCubeForFixture(
std::array<const char*, 6> fixture_names) const {
std::array<DecompressedImage, 6> images;
for (size_t i = 0; i < fixture_names.size(); i++) {
auto image = DecodeImageRGBA(
LoadFixtureImageCompressed(OpenAssetAsMapping(fixture_names[i])));
if (!image.has_value()) {
return nullptr;
}
images[i] = image.value();
}
auto texture_descriptor = TextureDescriptor{};
texture_descriptor.storage_mode = StorageMode::kHostVisible;
texture_descriptor.type = TextureType::kTextureCube;
texture_descriptor.format = PixelFormat::kR8G8B8A8UNormInt;
texture_descriptor.size = images[0].GetSize();
texture_descriptor.mip_count = 1u;
auto texture = renderer_->GetContext()->GetResourceAllocator()->CreateTexture(
texture_descriptor);
if (!texture) {
VALIDATION_LOG << "Could not allocate texture cube.";
return nullptr;
}
texture->SetLabel("Texture cube");
for (size_t i = 0; i < fixture_names.size(); i++) {
auto uploaded =
texture->SetContents(images[i].GetAllocation()->GetMapping(),
images[i].GetAllocation()->GetSize(), i);
if (!uploaded) {
VALIDATION_LOG << "Could not upload texture to device memory.";
return nullptr;
}
}
return texture;
}
void Playground::SetWindowSize(ISize size) {
window_size_ = size;
}
bool Playground::ShouldKeepRendering() const {
return true;
}
} // namespace impeller