From f3b445676aca039bd91a1c7457c88a33bbee02c0 Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Wed, 8 May 2024 10:40:27 -0700 Subject: [PATCH 1/2] revert 484688634c71ec38c18218eaa13dc47fa7007b74 --- ci/licenses_golden/licenses_flutter | 8 + impeller/BUILD.gn | 4 + impeller/aiks/BUILD.gn | 2 + impeller/aiks/aiks_playground.cc | 6 +- impeller/aiks/aiks_playground.h | 2 + impeller/aiks/aiks_playground_inspector.cc | 277 ++++++++++++++++ impeller/aiks/aiks_playground_inspector.h | 55 ++++ impeller/aiks/aiks_unittests.cc | 33 ++ impeller/core/BUILD.gn | 2 + impeller/core/capture.cc | 220 +++++++++++++ impeller/core/capture.h | 300 ++++++++++++++++++ impeller/entity/contents/content_context.h | 5 +- impeller/entity/contents/contents.cc | 3 +- .../entity/contents/solid_color_contents.cc | 3 +- impeller/entity/contents/texture_contents.cc | 21 +- impeller/entity/entity.cc | 8 + impeller/entity/entity.h | 6 + impeller/entity/entity_pass.cc | 36 ++- impeller/entity/entity_pass.h | 4 + impeller/entity/entity_pass_clip_stack.cc | 12 + impeller/renderer/context.cc | 4 +- impeller/renderer/context.h | 3 + impeller/tools/impeller.gni | 3 + shell/gpu/gpu_surface_gl_impeller.h | 1 - shell/gpu/gpu_surface_vulkan_impeller.h | 1 - testing/impeller_golden_tests_output.txt | 3 + 26 files changed, 1004 insertions(+), 18 deletions(-) create mode 100644 impeller/aiks/aiks_playground_inspector.cc create mode 100644 impeller/aiks/aiks_playground_inspector.h create mode 100644 impeller/core/capture.cc create mode 100644 impeller/core/capture.h diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4cea8511e761b..b8c15596edb2c 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -40210,6 +40210,8 @@ ORIGIN: ../../../flutter/impeller/aiks/aiks_context.cc + ../../../flutter/LICENS ORIGIN: ../../../flutter/impeller/aiks/aiks_context.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/aiks/aiks_playground.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/aiks/aiks_playground.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/aiks/aiks_playground_inspector.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/aiks/aiks_playground_inspector.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/aiks/canvas.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/aiks/canvas.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/aiks/canvas_benchmarks.cc + ../../../flutter/LICENSE @@ -40311,6 +40313,8 @@ ORIGIN: ../../../flutter/impeller/core/allocator.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/allocator.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/buffer_view.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/buffer_view.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/core/capture.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/core/capture.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/device_buffer.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/device_buffer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/device_buffer_descriptor.cc + ../../../flutter/LICENSE @@ -43088,6 +43092,8 @@ FILE: ../../../flutter/impeller/aiks/aiks_context.cc FILE: ../../../flutter/impeller/aiks/aiks_context.h FILE: ../../../flutter/impeller/aiks/aiks_playground.cc FILE: ../../../flutter/impeller/aiks/aiks_playground.h +FILE: ../../../flutter/impeller/aiks/aiks_playground_inspector.cc +FILE: ../../../flutter/impeller/aiks/aiks_playground_inspector.h FILE: ../../../flutter/impeller/aiks/canvas.cc FILE: ../../../flutter/impeller/aiks/canvas.h FILE: ../../../flutter/impeller/aiks/canvas_benchmarks.cc @@ -43189,6 +43195,8 @@ FILE: ../../../flutter/impeller/core/allocator.cc FILE: ../../../flutter/impeller/core/allocator.h FILE: ../../../flutter/impeller/core/buffer_view.cc FILE: ../../../flutter/impeller/core/buffer_view.h +FILE: ../../../flutter/impeller/core/capture.cc +FILE: ../../../flutter/impeller/core/capture.h FILE: ../../../flutter/impeller/core/device_buffer.cc FILE: ../../../flutter/impeller/core/device_buffer.h FILE: ../../../flutter/impeller/core/device_buffer_descriptor.cc diff --git a/impeller/BUILD.gn b/impeller/BUILD.gn index 8e7aee9978027..86a638abea5f8 100644 --- a/impeller/BUILD.gn +++ b/impeller/BUILD.gn @@ -14,6 +14,10 @@ config("impeller_public_config") { defines += [ "IMPELLER_DEBUG=1" ] } + if (impeller_capture) { + defines += [ "IMPELLER_ENABLE_CAPTURE=1" ] + } + if (impeller_supports_rendering) { defines += [ "IMPELLER_SUPPORTS_RENDERING=1" ] } diff --git a/impeller/aiks/BUILD.gn b/impeller/aiks/BUILD.gn index 591687172bf4a..8c6516821241c 100644 --- a/impeller/aiks/BUILD.gn +++ b/impeller/aiks/BUILD.gn @@ -57,6 +57,8 @@ impeller_component("aiks_playground") { sources = [ "aiks_playground.cc", "aiks_playground.h", + "aiks_playground_inspector.cc", + "aiks_playground_inspector.h", ] deps = [ ":aiks", diff --git a/impeller/aiks/aiks_playground.cc b/impeller/aiks/aiks_playground.cc index ddbbca1f4b298..db1545425f771 100644 --- a/impeller/aiks/aiks_playground.cc +++ b/impeller/aiks/aiks_playground.cc @@ -23,6 +23,7 @@ void AiksPlayground::SetTypographerContext( } void AiksPlayground::TearDown() { + inspector_.HackResetDueToTextureLeaks(); PlaygroundTest::TearDown(); } @@ -44,8 +45,9 @@ bool AiksPlayground::OpenPlaygroundHere(AiksPlaygroundCallback callback) { } return Playground::OpenPlaygroundHere( - [&renderer, &callback](RenderTarget& render_target) -> bool { - const std::optional& picture = callback(renderer); + [this, &renderer, &callback](RenderTarget& render_target) -> bool { + const std::optional& picture = inspector_.RenderInspector( + renderer, [&]() { return callback(renderer); }); if (!picture.has_value()) { return false; diff --git a/impeller/aiks/aiks_playground.h b/impeller/aiks/aiks_playground.h index ca3bc1b6de899..4d8a94693229e 100644 --- a/impeller/aiks/aiks_playground.h +++ b/impeller/aiks/aiks_playground.h @@ -6,6 +6,7 @@ #define FLUTTER_IMPELLER_AIKS_AIKS_PLAYGROUND_H_ #include "impeller/aiks/aiks_context.h" +#include "impeller/aiks/aiks_playground_inspector.h" #include "impeller/aiks/picture.h" #include "impeller/playground/playground_test.h" #include "impeller/typographer/typographer_context.h" @@ -37,6 +38,7 @@ class AiksPlayground : public PlaygroundTest { private: std::shared_ptr typographer_context_; + AiksInspector inspector_; AiksPlayground(const AiksPlayground&) = delete; diff --git a/impeller/aiks/aiks_playground_inspector.cc b/impeller/aiks/aiks_playground_inspector.cc new file mode 100644 index 0000000000000..89ace8c5d1283 --- /dev/null +++ b/impeller/aiks/aiks_playground_inspector.cc @@ -0,0 +1,277 @@ +// 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 "impeller/aiks/aiks_playground_inspector.h" + +#include + +#include "impeller/core/capture.h" +#include "impeller/entity/entity_pass.h" +#include "impeller/renderer/context.h" +#include "third_party/imgui/imgui.h" +#include "third_party/imgui/imgui_internal.h" + +namespace impeller { + +static const char* kElementsWindowName = "Elements"; +static const char* kPropertiesWindowName = "Properties"; + +static const std::initializer_list kSupportedDocuments = { + EntityPass::kCaptureDocumentName}; + +AiksInspector::AiksInspector() = default; + +const std::optional& AiksInspector::RenderInspector( + AiksContext& aiks_context, + const std::function()>& picture_callback) { + //---------------------------------------------------------------------------- + /// Configure the next frame. + /// + + RenderCapture(aiks_context.GetContext()->capture); + + //---------------------------------------------------------------------------- + /// Configure the next frame. + /// + + if (ImGui::IsKeyPressed(ImGuiKey_Z)) { + wireframe_ = !wireframe_; + aiks_context.GetContentContext().SetWireframe(wireframe_); + } + + if (ImGui::IsKeyPressed(ImGuiKey_C)) { + capturing_ = !capturing_; + if (capturing_) { + aiks_context.GetContext()->capture = + CaptureContext::MakeAllowlist({kSupportedDocuments}); + } + } + if (!capturing_) { + hovered_element_ = nullptr; + selected_element_ = nullptr; + aiks_context.GetContext()->capture = CaptureContext::MakeInactive(); + std::optional new_picture = picture_callback(); + + // If the new picture doesn't have a pass, that means it was already moved + // into the inspector. Simply re-emit the last received valid picture. + if (!new_picture.has_value() || new_picture->pass) { + last_picture_ = std::move(new_picture); + } + } + + return last_picture_; +} + +void AiksInspector::HackResetDueToTextureLeaks() { + last_picture_.reset(); +} + +static const auto kPropertiesProcTable = CaptureProcTable{ + .boolean = + [](CaptureBooleanProperty& p) { + ImGui::Checkbox(p.label.c_str(), &p.value); + }, + .integer = + [](CaptureIntegerProperty& p) { + if (p.options.range.has_value()) { + ImGui::SliderInt(p.label.c_str(), &p.value, + static_cast(p.options.range->min), + static_cast(p.options.range->max)); + return; + } + ImGui::InputInt(p.label.c_str(), &p.value); + }, + .scalar = + [](CaptureScalarProperty& p) { + if (p.options.range.has_value()) { + ImGui::SliderFloat(p.label.c_str(), &p.value, p.options.range->min, + p.options.range->max); + return; + } + ImGui::DragFloat(p.label.c_str(), &p.value, 0.01); + }, + .point = + [](CapturePointProperty& p) { + if (p.options.range.has_value()) { + ImGui::SliderFloat2(p.label.c_str(), + reinterpret_cast(&p.value), + p.options.range->min, p.options.range->max); + return; + } + ImGui::DragFloat2(p.label.c_str(), reinterpret_cast(&p.value), + 0.01); + }, + .vector3 = + [](CaptureVector3Property& p) { + if (p.options.range.has_value()) { + ImGui::SliderFloat3(p.label.c_str(), + reinterpret_cast(&p.value), + p.options.range->min, p.options.range->max); + return; + } + ImGui::DragFloat3(p.label.c_str(), reinterpret_cast(&p.value), + 0.01); + }, + .rect = + [](CaptureRectProperty& p) { + ImGui::DragFloat4(p.label.c_str(), reinterpret_cast(&p.value), + 0.01); + }, + .color = + [](CaptureColorProperty& p) { + ImGui::ColorEdit4(p.label.c_str(), + reinterpret_cast(&p.value)); + }, + .matrix = + [](CaptureMatrixProperty& p) { + float* pointer = reinterpret_cast(&p.value); + ImGui::DragFloat4((p.label + " X basis").c_str(), pointer, 0.001); + ImGui::DragFloat4((p.label + " Y basis").c_str(), pointer + 4, 0.001); + ImGui::DragFloat4((p.label + " Z basis").c_str(), pointer + 8, 0.001); + ImGui::DragFloat4((p.label + " Translation").c_str(), pointer + 12, + 0.001); + }, + .string = + [](CaptureStringProperty& p) { + ImGui::InputTextEx(p.label.c_str(), "", + // Fine as long as it's read-only. + const_cast(p.value.c_str()), p.value.size(), + ImVec2(0, 0), ImGuiInputTextFlags_ReadOnly); + }, +}; + +void AiksInspector::RenderCapture(CaptureContext& capture_context) { + if (!capturing_) { + return; + } + + auto document = capture_context.GetDocument(EntityPass::kCaptureDocumentName); + + //---------------------------------------------------------------------------- + /// Setup a shared dockspace to collect the capture windows. + /// + + ImGui::SetNextWindowBgAlpha(0.5); + ImGui::Begin("Capture"); + auto dockspace_id = ImGui::GetID("CaptureDockspace"); + if (!ImGui::DockBuilderGetNode(dockspace_id)) { + ImGui::SetWindowSize(ImVec2(370, 680)); + ImGui::SetWindowPos(ImVec2(640, 55)); + + ImGui::DockBuilderRemoveNode(dockspace_id); + ImGui::DockBuilderAddNode(dockspace_id); + + ImGuiID opposite_id; + ImGuiID up_id = ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.6, + nullptr, &opposite_id); + ImGuiID down_id = ImGui::DockBuilderSplitNode(opposite_id, ImGuiDir_Down, + 0.0, nullptr, nullptr); + ImGui::DockBuilderDockWindow(kElementsWindowName, up_id); + ImGui::DockBuilderDockWindow(kPropertiesWindowName, down_id); + + ImGui::DockBuilderFinish(dockspace_id); + } + ImGui::DockSpace(dockspace_id); + ImGui::End(); // Capture window. + + //---------------------------------------------------------------------------- + /// Element hierarchy window. + /// + + ImGui::Begin(kElementsWindowName); + auto root_element = document.GetElement(); + hovered_element_ = nullptr; + if (root_element) { + RenderCaptureElement(*root_element); + } + ImGui::End(); // Hierarchy window. + + if (selected_element_) { + //---------------------------------------------------------------------------- + /// Properties window. + /// + + ImGui::Begin(kPropertiesWindowName); + { + selected_element_->properties.Iterate([&](CaptureProperty& property) { + property.Invoke(kPropertiesProcTable); + }); + } + ImGui::End(); // Inspector window. + + //---------------------------------------------------------------------------- + /// Selected coverage highlighting. + /// + + auto coverage_property = + selected_element_->properties.FindFirstByLabel("Coverage"); + if (coverage_property) { + auto coverage = coverage_property->AsRect(); + if (coverage.has_value()) { + Scalar scale = ImGui::GetWindowDpiScale(); + ImGui::GetBackgroundDrawList()->AddRect( + ImVec2(coverage->GetLeft() / scale, + coverage->GetTop() / scale), // p_min + ImVec2(coverage->GetRight() / scale, + coverage->GetBottom() / scale), // p_max + 0x992222FF, // col + 0.0, // rounding + ImDrawFlags_None, // flags + 8.0); // thickness + } + } + } + + //---------------------------------------------------------------------------- + /// Hover coverage highlight. + /// + + if (hovered_element_) { + auto coverage_property = + hovered_element_->properties.FindFirstByLabel("Coverage"); + if (coverage_property) { + auto coverage = coverage_property->AsRect(); + if (coverage.has_value()) { + Scalar scale = ImGui::GetWindowDpiScale(); + ImGui::GetBackgroundDrawList()->AddRect( + ImVec2(coverage->GetLeft() / scale, + coverage->GetTop() / scale), // p_min + ImVec2(coverage->GetRight() / scale, + coverage->GetBottom() / scale), // p_max + 0x66FF2222, // col + 0.0, // rounding + ImDrawFlags_None, // flags + 8.0); // thickness + } + } + } +} + +void AiksInspector::RenderCaptureElement(CaptureElement& element) { + ImGui::PushID(&element); + + bool is_selected = selected_element_ == &element; + bool has_children = element.children.Count() > 0; + + bool opened = ImGui::TreeNodeEx( + element.label.c_str(), (is_selected ? ImGuiTreeNodeFlags_Selected : 0) | + (has_children ? 0 : ImGuiTreeNodeFlags_Leaf) | + ImGuiTreeNodeFlags_SpanFullWidth | + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_DefaultOpen); + if (ImGui::IsItemClicked()) { + selected_element_ = &element; + } + if (ImGui::IsItemHovered()) { + hovered_element_ = &element; + } + if (opened) { + element.children.Iterate( + [&](CaptureElement& child) { RenderCaptureElement(child); }); + ImGui::TreePop(); + } + ImGui::PopID(); +} + +} // namespace impeller diff --git a/impeller/aiks/aiks_playground_inspector.h b/impeller/aiks/aiks_playground_inspector.h new file mode 100644 index 0000000000000..297fb16b64e69 --- /dev/null +++ b/impeller/aiks/aiks_playground_inspector.h @@ -0,0 +1,55 @@ +// 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_IMPELLER_AIKS_AIKS_PLAYGROUND_INSPECTOR_H_ +#define FLUTTER_IMPELLER_AIKS_AIKS_PLAYGROUND_INSPECTOR_H_ + +#include +#include + +#include "flutter/fml/macros.h" +#include "impeller/aiks/aiks_context.h" +#include "impeller/aiks/picture.h" +#include "impeller/core/capture.h" +#include "impeller/renderer/context.h" + +namespace impeller { + +class AiksInspector { + public: + AiksInspector(); + + const std::optional& RenderInspector( + AiksContext& aiks_context, + const std::function()>& picture_callback); + + // Resets (releases) the underlying |Picture| object. + // + // Underlying issue: . + // + // The tear-down code is not running in the right order; we still have a + // reference to the |Picture| object when the |Context| is being destroyed, + // which causes the |Texture| objects to leak. + // + // TODO(matanlurey): https://github.com/flutter/flutter/issues/134748. + void HackResetDueToTextureLeaks(); + + private: + void RenderCapture(CaptureContext& capture_context); + void RenderCaptureElement(CaptureElement& element); + + bool capturing_ = false; + bool wireframe_ = false; + CaptureElement* hovered_element_ = nullptr; + CaptureElement* selected_element_ = nullptr; + std::optional last_picture_; + + AiksInspector(const AiksInspector&) = delete; + + AiksInspector& operator=(const AiksInspector&) = delete; +}; + +}; // namespace impeller + +#endif // FLUTTER_IMPELLER_AIKS_AIKS_PLAYGROUND_INSPECTOR_H_ diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index b214c91d027a8..a71ec0dad8e30 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -21,6 +21,7 @@ #include "impeller/aiks/paint_pass_delegate.h" #include "impeller/aiks/testing/context_spy.h" #include "impeller/core/device_buffer.h" +#include "impeller/core/capture.h" #include "impeller/entity/contents/solid_color_contents.h" #include "impeller/geometry/color.h" #include "impeller/geometry/constants.h" @@ -2595,6 +2596,38 @@ TEST_P(AiksTest, PipelineBlendSingleParameter) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, CaptureContext) { + auto capture_context = CaptureContext::MakeAllowlist({"TestDocument"}); + + auto callback = [&](AiksContext& renderer) -> std::optional { + Canvas canvas; + + capture_context.Rewind(); + auto document = capture_context.GetDocument("TestDocument"); + + auto color = document.AddColor("Background color", Color::CornflowerBlue()); + canvas.DrawPaint({.color = color}); + + if (AiksTest::ImGuiBegin("TestDocument", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + document.GetElement()->properties.Iterate([](CaptureProperty& property) { + property.Invoke({.color = [](CaptureColorProperty& p) { + ImGui::ColorEdit4(p.label.c_str(), + reinterpret_cast(&p.value)); + }}); + }); + ImGui::End(); + } + + return canvas.EndRecordingAsPicture(); + }; + OpenPlaygroundHere(callback); +} + +TEST_P(AiksTest, CaptureInactivatedByDefault) { + ASSERT_FALSE(GetContext()->capture.IsActive()); +} + // Regression test for https://github.com/flutter/flutter/issues/134678. TEST_P(AiksTest, ReleasesTextureOnTeardown) { auto context = MakeContext(); diff --git a/impeller/core/BUILD.gn b/impeller/core/BUILD.gn index e526c46115313..e4587bda12a83 100644 --- a/impeller/core/BUILD.gn +++ b/impeller/core/BUILD.gn @@ -10,6 +10,8 @@ impeller_component("core") { "allocator.h", "buffer_view.cc", "buffer_view.h", + "capture.cc", + "capture.h", "device_buffer.cc", "device_buffer.h", "device_buffer_descriptor.cc", diff --git a/impeller/core/capture.cc b/impeller/core/capture.cc new file mode 100644 index 0000000000000..a4b4c462b829b --- /dev/null +++ b/impeller/core/capture.cc @@ -0,0 +1,220 @@ +// 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 "impeller/core/capture.h" + +#include +#include + +namespace impeller { + +//----------------------------------------------------------------------------- +/// CaptureProperty +/// + +CaptureProperty::CaptureProperty(const std::string& label, Options options) + : CaptureCursorListElement(label), options(options) {} + +CaptureProperty::~CaptureProperty() = default; + +bool CaptureProperty::MatchesCloselyEnough(const CaptureProperty& other) const { + if (label != other.label) { + return false; + } + if (GetType() != other.GetType()) { + return false; + } + return true; +} + +#define _CAPTURE_PROPERTY_CAST_DEFINITION(type_name, pascal_name, lower_name) \ + std::optional CaptureProperty::As##pascal_name() const { \ + if (GetType() != Type::k##pascal_name) { \ + return std::nullopt; \ + } \ + return reinterpret_cast(this) \ + ->value; \ + } + +_FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_CAST_DEFINITION); + +#define _CAPTURE_PROPERTY_DEFINITION(type_name, pascal_name, lower_name) \ + Capture##pascal_name##Property::Capture##pascal_name##Property( \ + const std::string& label, type_name value, Options options) \ + : CaptureProperty(label, options), value(std::move(value)) {} \ + \ + std::shared_ptr \ + Capture##pascal_name##Property::Make(const std::string& label, \ + type_name value, Options options) { \ + auto result = std::shared_ptr( \ + new Capture##pascal_name##Property(label, std::move(value), options)); \ + return result; \ + } \ + \ + CaptureProperty::Type Capture##pascal_name##Property::GetType() const { \ + return Type::k##pascal_name; \ + } \ + \ + void Capture##pascal_name##Property::Invoke( \ + const CaptureProcTable& proc_table) { \ + proc_table.lower_name(*this); \ + } + +_FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_DEFINITION); + +//----------------------------------------------------------------------------- +/// CaptureElement +/// + +CaptureElement::CaptureElement(const std::string& label) + : CaptureCursorListElement(label) {} + +std::shared_ptr CaptureElement::Make(const std::string& label) { + return std::shared_ptr(new CaptureElement(label)); +} + +void CaptureElement::Rewind() { + properties.Rewind(); + children.Rewind(); +} + +bool CaptureElement::MatchesCloselyEnough(const CaptureElement& other) const { + return label == other.label; +} + +//----------------------------------------------------------------------------- +/// Capture +/// + +Capture::Capture() = default; + +#ifdef IMPELLER_ENABLE_CAPTURE +Capture::Capture(const std::string& label) + : element_(CaptureElement::Make(label)), active_(true) { + element_->label = label; +} +#else +Capture::Capture(const std::string& label) {} +#endif + +Capture Capture::MakeInactive() { + return Capture(); +} + +std::shared_ptr Capture::GetElement() const { +#ifdef IMPELLER_ENABLE_CAPTURE + return element_; +#else + return nullptr; +#endif +} + +void Capture::Rewind() { + return GetElement()->Rewind(); +} + +#ifdef IMPELLER_ENABLE_CAPTURE +#define _CAPTURE_PROPERTY_RECORDER_DEFINITION(type_name, pascal_name, \ + lower_name) \ + type_name Capture::Add##pascal_name(std::string_view label, type_name value, \ + CaptureProperty::Options options) { \ + if (!active_) { \ + return value; \ + } \ + FML_DCHECK(element_ != nullptr); \ + \ + std::string label_clone = std::string(label); \ + auto new_value = Capture##pascal_name##Property::Make( \ + label_clone, std::move(value), options); \ + \ + auto next = std::reinterpret_pointer_cast( \ + element_->properties.GetNext(std::move(new_value), options.readonly)); \ + \ + return next->value; \ + } + +_FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_RECORDER_DEFINITION); +#endif + +//----------------------------------------------------------------------------- +/// CaptureContext +/// + +#ifdef IMPELLER_ENABLE_CAPTURE +CaptureContext::CaptureContext() : active_(true) {} +CaptureContext::CaptureContext(std::initializer_list allowlist) + : active_(true), allowlist_(allowlist) {} +#else +CaptureContext::CaptureContext() {} +CaptureContext::CaptureContext(std::initializer_list allowlist) {} +#endif + +CaptureContext::CaptureContext(CaptureContext::InactiveFlag) {} + +CaptureContext CaptureContext::MakeInactive() { + return CaptureContext(InactiveFlag{}); +} + +CaptureContext CaptureContext::MakeAllowlist( + std::initializer_list allowlist) { + return CaptureContext(allowlist); +} + +bool CaptureContext::IsActive() const { +#ifdef IMPELLER_ENABLE_CAPTURE + return active_; +#else + return false; +#endif +} + +void CaptureContext::Rewind() { +#ifdef IMPELLER_ENABLE_CAPTURE + for (auto& [name, capture] : documents_) { + capture.GetElement()->Rewind(); + } +#else + return; +#endif +} + +Capture CaptureContext::GetDocument(const std::string& label) { +#ifdef IMPELLER_ENABLE_CAPTURE + if (!active_) { + return Capture::MakeInactive(); + } + + if (allowlist_.has_value()) { + if (allowlist_->find(label) == allowlist_->end()) { + return Capture::MakeInactive(); + } + } + + auto found = documents_.find(label); + if (found != documents_.end()) { + // Always rewind when fetching an existing document. + found->second.Rewind(); + return found->second; + } + + auto new_document = Capture(label); + documents_.emplace(label, new_document); + return new_document; +#else + return Capture::MakeInactive(); +#endif +} + +bool CaptureContext::DoesDocumentExist(const std::string& label) const { +#ifdef IMPELLER_ENABLE_CAPTURE + if (!active_) { + return false; + } + return documents_.find(label) != documents_.end(); +#else + return false; +#endif +} + +} // namespace impeller diff --git a/impeller/core/capture.h b/impeller/core/capture.h new file mode 100644 index 0000000000000..9e5c0d9318417 --- /dev/null +++ b/impeller/core/capture.h @@ -0,0 +1,300 @@ +// 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_IMPELLER_CORE_CAPTURE_H_ +#define FLUTTER_IMPELLER_CORE_CAPTURE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flutter/fml/logging.h" +#include "flutter/fml/macros.h" +#include "impeller/geometry/color.h" +#include "impeller/geometry/matrix.h" +#include "impeller/geometry/point.h" +#include "impeller/geometry/rect.h" +#include "impeller/geometry/scalar.h" +#include "impeller/geometry/vector.h" + +namespace impeller { + +struct CaptureProcTable; + +#define _FOR_EACH_CAPTURE_PROPERTY(PROPERTY_V) \ + PROPERTY_V(bool, Boolean, boolean) \ + PROPERTY_V(int, Integer, integer) \ + PROPERTY_V(Scalar, Scalar, scalar) \ + PROPERTY_V(Point, Point, point) \ + PROPERTY_V(Vector3, Vector3, vector3) \ + PROPERTY_V(Rect, Rect, rect) \ + PROPERTY_V(Color, Color, color) \ + PROPERTY_V(Matrix, Matrix, matrix) \ + PROPERTY_V(std::string, String, string) + +template +struct CaptureCursorListElement { + std::string label; + + explicit CaptureCursorListElement(const std::string& label) : label(label){}; + + virtual ~CaptureCursorListElement() = default; + + //---------------------------------------------------------------------------- + /// @brief Determines if previously captured data matches closely enough with + /// newly recorded data to safely emitted in its place. If this + /// returns `false`, then the remaining elements in the capture list + /// are discarded and re-recorded. + /// + /// This mechanism ensures that the UI of an interactive inspector can + /// never deviate from reality, even if the schema of the captured + /// data were to significantly deviate. + /// + virtual bool MatchesCloselyEnough(const Type& other) const = 0; +}; + +#define _CAPTURE_TYPE(type_name, pascal_name, lower_name) k##pascal_name, + +#define _CAPTURE_PROPERTY_CAST_DECLARATION(type_name, pascal_name, lower_name) \ + std::optional As##pascal_name() const; + +/// A capturable property type +struct CaptureProperty : public CaptureCursorListElement { + enum class Type { _FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_TYPE) }; + + struct Options { + struct Range { + Scalar min; + Scalar max; + }; + + /// Readonly properties are always re-recorded during capture. Any edits + /// made to readonly values in-between captures are overwritten during the + /// next capture. + bool readonly = false; + + /// An inspector hint that can be used for displaying sliders. Only used for + /// numeric types. Rounded down for integer types. + std::optional range; + }; + + Options options; + + CaptureProperty(const std::string& label, Options options); + + virtual ~CaptureProperty(); + + virtual Type GetType() const = 0; + + virtual void Invoke(const CaptureProcTable& proc_table) = 0; + + bool MatchesCloselyEnough(const CaptureProperty& other) const override; + + _FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_CAST_DECLARATION) +}; + +#define _CAPTURE_PROPERTY_DECLARATION(type_name, pascal_name, lower_name) \ + struct Capture##pascal_name##Property final : public CaptureProperty { \ + type_name value; \ + \ + static std::shared_ptr \ + Make(const std::string& label, type_name value, Options options); \ + \ + /* |CaptureProperty| */ \ + Type GetType() const override; \ + \ + /* |CaptureProperty| */ \ + void Invoke(const CaptureProcTable& proc_table) override; \ + \ + private: \ + Capture##pascal_name##Property(const std::string& label, \ + type_name value, \ + Options options); \ + \ + FML_DISALLOW_COPY_AND_ASSIGN(Capture##pascal_name##Property); \ + }; + +_FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_DECLARATION); + +#define _CAPTURE_PROC(type_name, pascal_name, lower_name) \ + std::function lower_name = \ + [](Capture##pascal_name##Property& value) {}; + +struct CaptureProcTable { + _FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROC) +}; + +template +class CapturePlaybackList { + public: + CapturePlaybackList() = default; + + ~CapturePlaybackList() { + // Force the list element type to inherit the CRTP type. We can't enforce + // this as a template requirement directly because `CaptureElement` has a + // recursive `CaptureCursorList` property, and so the + // compiler fails the check due to the type being incomplete. + static_assert(std::is_base_of_v, Type>); + } + + void Rewind() { cursor_ = 0; } + + size_t Count() { return values_.size(); } + + std::shared_ptr GetNext(std::shared_ptr captured, + bool force_overwrite) { + if (cursor_ < values_.size()) { + std::shared_ptr& result = values_[cursor_]; + + if (result->MatchesCloselyEnough(*captured)) { + if (force_overwrite) { + values_[cursor_] = captured; + } + // Safe playback is possible. + ++cursor_; + return result; + } + // The data has changed too much from the last capture to safely continue + // playback. Discard this and all subsequent elements to re-record. + values_.resize(cursor_); + } + + ++cursor_; + values_.push_back(captured); + return captured; + } + + std::shared_ptr FindFirstByLabel(const std::string& label) { + for (std::shared_ptr& value : values_) { + if (value->label == label) { + return value; + } + } + return nullptr; + } + + void Iterate(std::function iterator) const { + for (auto& value : values_) { + iterator(*value); + } + } + + private: + size_t cursor_ = 0; + std::vector> values_; + + CapturePlaybackList(const CapturePlaybackList&) = delete; + + CapturePlaybackList& operator=(const CapturePlaybackList&) = delete; +}; + +/// A document of capture data, containing a list of properties and a list +/// of subdocuments. +struct CaptureElement final : public CaptureCursorListElement { + CapturePlaybackList properties; + CapturePlaybackList children; + + static std::shared_ptr Make(const std::string& label); + + void Rewind(); + + bool MatchesCloselyEnough(const CaptureElement& other) const override; + + private: + explicit CaptureElement(const std::string& label); + + CaptureElement(const CaptureElement&) = delete; + + CaptureElement& operator=(const CaptureElement&) = delete; +}; + +#ifdef IMPELLER_ENABLE_CAPTURE +#define _CAPTURE_PROPERTY_RECORDER_DECLARATION(type_name, pascal_name, \ + lower_name) \ + type_name Add##pascal_name(std::string_view label, type_name value, \ + CaptureProperty::Options options = {}); +#else +#define _CAPTURE_PROPERTY_RECORDER_DECLARATION(type_name, pascal_name, \ + lower_name) \ + inline type_name Add##pascal_name(std::string_view label, type_name value, \ + CaptureProperty::Options options = {}) { \ + return value; \ + } +#endif + +class Capture { + public: + explicit Capture(const std::string& label); + + Capture(); + + static Capture MakeInactive(); + + inline Capture CreateChild(std::string_view label) { +#ifdef IMPELLER_ENABLE_CAPTURE + if (!active_) { + return Capture(); + } + + std::string label_copy = std::string(label); + auto new_capture = Capture(label_copy); + new_capture.element_ = + element_->children.GetNext(new_capture.element_, false); + new_capture.element_->Rewind(); + return new_capture; +#else + return Capture(); +#endif + } + + std::shared_ptr GetElement() const; + + void Rewind(); + + _FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_RECORDER_DECLARATION) + + private: +#ifdef IMPELLER_ENABLE_CAPTURE + std::shared_ptr element_; + bool active_ = false; +#endif +}; + +class CaptureContext { + public: + CaptureContext(); + + static CaptureContext MakeInactive(); + + static CaptureContext MakeAllowlist( + std::initializer_list allowlist); + + bool IsActive() const; + + void Rewind(); + + Capture GetDocument(const std::string& label); + + bool DoesDocumentExist(const std::string& label) const; + + private: + struct InactiveFlag {}; + explicit CaptureContext(InactiveFlag); + CaptureContext(std::initializer_list allowlist); + +#ifdef IMPELLER_ENABLE_CAPTURE + bool active_ = false; + std::optional> allowlist_; + std::unordered_map documents_; +#endif +}; + +} // namespace impeller + +#endif // FLUTTER_IMPELLER_CORE_CAPTURE_H_ diff --git a/impeller/entity/contents/content_context.h b/impeller/entity/contents/content_context.h index aa173c3b04fc1..1888845a222f8 100644 --- a/impeller/entity/contents/content_context.h +++ b/impeller/entity/contents/content_context.h @@ -10,17 +10,18 @@ #include #include +#include "flutter/fml/build_config.h" #include "flutter/fml/logging.h" #include "flutter/fml/status_or.h" #include "impeller/base/validation.h" #include "impeller/core/formats.h" #include "impeller/core/host_buffer.h" +#include "impeller/entity/entity.h" #include "impeller/renderer/capabilities.h" #include "impeller/renderer/command_buffer.h" #include "impeller/renderer/pipeline.h" #include "impeller/renderer/pipeline_descriptor.h" #include "impeller/renderer/render_target.h" -#include "impeller/typographer/lazy_glyph_atlas.h" #include "impeller/typographer/typographer_context.h" #include "impeller/entity/border_mask_blur.frag.h" @@ -53,6 +54,8 @@ #include "impeller/entity/tiled_texture_fill.frag.h" #include "impeller/entity/yuv_to_rgb_filter.frag.h" +#include "impeller/typographer/glyph_atlas.h" + #include "impeller/entity/conical_gradient_ssbo_fill.frag.h" #include "impeller/entity/linear_gradient_ssbo_fill.frag.h" #include "impeller/entity/radial_gradient_ssbo_fill.frag.h" diff --git a/impeller/entity/contents/contents.cc b/impeller/entity/contents/contents.cc index 421713761175e..2c24482a544eb 100644 --- a/impeller/entity/contents/contents.cc +++ b/impeller/entity/contents/contents.cc @@ -6,11 +6,12 @@ #include #include "fml/logging.h" +#include "impeller/base/strings.h" #include "impeller/base/validation.h" #include "impeller/core/formats.h" #include "impeller/entity/contents/anonymous_contents.h" #include "impeller/entity/contents/content_context.h" -#include "impeller/entity/entity.h" +#include "impeller/entity/contents/texture_contents.h" #include "impeller/renderer/command_buffer.h" #include "impeller/renderer/render_pass.h" diff --git a/impeller/entity/contents/solid_color_contents.cc b/impeller/entity/contents/solid_color_contents.cc index 0c99d47990b7b..083ac7d61ec38 100644 --- a/impeller/entity/contents/solid_color_contents.cc +++ b/impeller/entity/contents/solid_color_contents.cc @@ -48,10 +48,11 @@ std::optional SolidColorContents::GetCoverage( bool SolidColorContents::Render(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { + auto capture = entity.GetCapture().CreateChild("SolidColorContents"); using VS = SolidFillPipeline::VertexShader; VS::FrameInfo frame_info; - frame_info.color = GetColor().Premultiply(); + frame_info.color = capture.AddColor("Color", GetColor()).Premultiply(); PipelineBuilderCallback pipeline_callback = [&renderer](ContentContextOptions options) { diff --git a/impeller/entity/contents/texture_contents.cc b/impeller/entity/contents/texture_contents.cc index a2af33e96c486..72fce8d0ff211 100644 --- a/impeller/entity/contents/texture_contents.cc +++ b/impeller/entity/contents/texture_contents.cc @@ -109,6 +109,8 @@ std::optional TextureContents::RenderToSnapshot( bool TextureContents::Render(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { + auto capture = entity.GetCapture().CreateChild("TextureContents"); + using VS = TextureFillVertexShader; using FS = TextureFillFragmentShader; using FSStrict = TextureFillStrictSrcFragmentShader; @@ -122,15 +124,18 @@ bool TextureContents::Render(const ContentContext& renderer, texture_->GetTextureDescriptor().type == TextureType::kTextureExternalOES; FML_DCHECK(!is_external_texture); + auto source_rect = capture.AddRect("Source rect", source_rect_); auto texture_coords = - Rect::MakeSize(texture_->GetSize()).Project(source_rect_); + Rect::MakeSize(texture_->GetSize()).Project(source_rect); VertexBufferBuilder vertex_builder; + auto destination_rect = + capture.AddRect("Destination rect", destination_rect_); vertex_builder.AddVertices({ - {destination_rect_.GetLeftTop(), texture_coords.GetLeftTop()}, - {destination_rect_.GetRightTop(), texture_coords.GetRightTop()}, - {destination_rect_.GetLeftBottom(), texture_coords.GetLeftBottom()}, - {destination_rect_.GetRightBottom(), texture_coords.GetRightBottom()}, + {destination_rect.GetLeftTop(), texture_coords.GetLeftTop()}, + {destination_rect.GetRightTop(), texture_coords.GetRightTop()}, + {destination_rect.GetLeftBottom(), texture_coords.GetLeftBottom()}, + {destination_rect.GetRightBottom(), texture_coords.GetRightBottom()}, }); auto& host_buffer = renderer.GetTransientsBuffer(); @@ -165,11 +170,11 @@ bool TextureContents::Render(const ContentContext& renderer, // texel to ensure that linear filtering does not sample anything outside // the source rect bounds. auto strict_texture_coords = - Rect::MakeSize(texture_->GetSize()).Project(source_rect_.Expand(-0.5)); + Rect::MakeSize(texture_->GetSize()).Project(source_rect.Expand(-0.5)); FSStrict::FragInfo frag_info; frag_info.source_rect = Vector4(strict_texture_coords.GetLTRB()); - frag_info.alpha = GetOpacity(); + frag_info.alpha = capture.AddScalar("Alpha", GetOpacity()); FSStrict::BindFragInfo(pass, host_buffer.EmplaceUniform((frag_info))); FSStrict::BindTextureSampler( pass, texture_, @@ -177,7 +182,7 @@ bool TextureContents::Render(const ContentContext& renderer, sampler_descriptor_)); } else { FS::FragInfo frag_info; - frag_info.alpha = GetOpacity(); + frag_info.alpha = capture.AddScalar("Alpha", GetOpacity()); FS::BindFragInfo(pass, host_buffer.EmplaceUniform((frag_info))); FS::BindTextureSampler( pass, texture_, diff --git a/impeller/entity/entity.cc b/impeller/entity/entity.cc index cb336892d3a61..9111a61d29318 100644 --- a/impeller/entity/entity.cc +++ b/impeller/entity/entity.cc @@ -188,8 +188,16 @@ Scalar Entity::DeriveTextScale() const { return GetTransform().GetMaxBasisLengthXY(); } +Capture& Entity::GetCapture() const { + return capture_; +} + Entity Entity::Clone() const { return Entity(*this); } +void Entity::SetCapture(Capture capture) const { + capture_ = std::move(capture); +} + } // namespace impeller diff --git a/impeller/entity/entity.h b/impeller/entity/entity.h index 0161d7153eaac..d2d4eb9596a64 100644 --- a/impeller/entity/entity.h +++ b/impeller/entity/entity.h @@ -7,6 +7,7 @@ #include +#include "impeller/core/capture.h" #include "impeller/entity/contents/contents.h" #include "impeller/geometry/color.h" #include "impeller/geometry/matrix.h" @@ -122,6 +123,10 @@ class Entity { Scalar DeriveTextScale() const; + Capture& GetCapture() const; + + void SetCapture(Capture capture) const; + Entity Clone() const; private: @@ -131,6 +136,7 @@ class Entity { std::shared_ptr contents_; BlendMode blend_mode_ = BlendMode::kSourceOver; uint32_t clip_depth_ = 1u; + mutable Capture capture_; }; } // namespace impeller diff --git a/impeller/entity/entity_pass.cc b/impeller/entity/entity_pass.cc index 4ed42ceeb0cd8..596f4412daf84 100644 --- a/impeller/entity/entity_pass.cc +++ b/impeller/entity/entity_pass.cc @@ -44,6 +44,8 @@ std::tuple, BlendMode> ElementAsBackgroundColor( } } // namespace +const std::string EntityPass::kCaptureDocumentName = "EntityPass"; + EntityPass::EntityPass() = default; EntityPass::~EntityPass() = default; @@ -352,6 +354,9 @@ bool EntityPass::DoesBackdropGetRead(ContentContext& renderer) const { bool EntityPass::Render(ContentContext& renderer, const RenderTarget& render_target) const { + auto capture = + renderer.GetContext()->capture.GetDocument(kCaptureDocumentName); + renderer.GetRenderTargetCache()->Start(); fml::ScopedCleanupClosure reset_state([&renderer]() { renderer.GetLazyGlyphAtlas()->ResetTextFrames(); @@ -372,6 +377,10 @@ bool EntityPass::Render(ContentContext& renderer, return false; } + capture.AddRect("Coverage", + Rect::MakeSize(root_render_target.GetRenderTargetSize()), + {.readonly = true}); + const auto& lazy_glyph_atlas = renderer.GetLazyGlyphAtlas(); IterateAllEntities([&lazy_glyph_atlas](const Entity& entity) { if (const auto& contents = entity.GetContents()) { @@ -393,6 +402,7 @@ bool EntityPass::Render(ContentContext& renderer, GetClearColorOrDefault(render_target.GetRenderTargetSize())); if (!OnRender(renderer, // renderer + capture, // capture offscreen_target.GetRenderTarget() .GetRenderTargetSize(), // root_pass_size offscreen_target, // pass_target @@ -499,6 +509,7 @@ bool EntityPass::Render(ContentContext& renderer, return OnRender( // renderer, // renderer + capture, // capture root_render_target.GetRenderTargetSize(), // root_pass_size pass_target, // pass_target Point(), // global_pass_position @@ -510,6 +521,7 @@ bool EntityPass::Render(ContentContext& renderer, EntityPass::EntityResult EntityPass::GetEntityForElement( const EntityPass::Element& element, ContentContext& renderer, + Capture& capture, InlinePassContext& pass_context, ISize root_pass_size, Point global_pass_position, @@ -521,6 +533,7 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( /// if (const auto& entity = std::get_if(&element)) { Entity element_entity = entity->Clone(); + element_entity.SetCapture(capture.CreateChild("Entity")); if (!global_pass_position.IsZero()) { // If the pass image is going to be rendered with a non-zero position, @@ -545,9 +558,11 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( if (!subpass->backdrop_filter_proc_ && subpass->delegate_->CanCollapseIntoParentPass(subpass)) { + auto subpass_capture = capture.CreateChild("EntityPass (Collapsed)"); // Directly render into the parent target and move on. if (!subpass->OnRender( renderer, // renderer + subpass_capture, // capture root_pass_size, // root_pass_size pass_context.GetPassTarget(), // pass_target global_pass_position, // global_pass_position @@ -591,10 +606,12 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( if (!clip_coverage_stack.HasCoverage()) { // The current clip is empty. This means the pass texture won't be // visible, so skip it. + capture.CreateChild("Subpass Entity (Skipped: Empty clip A)"); return EntityPass::EntityResult::Skip(); } auto clip_coverage_back = clip_coverage_stack.CurrentClipCoverage(); if (!clip_coverage_back.has_value()) { + capture.CreateChild("Subpass Entity (Skipped: Empty clip B)"); return EntityPass::EntityResult::Skip(); } @@ -606,12 +623,14 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( .GetRenderTargetSize())) .Intersection(clip_coverage_back.value()); if (!coverage_limit.has_value()) { + capture.CreateChild("Subpass Entity (Skipped: Empty coverage limit A)"); return EntityPass::EntityResult::Skip(); } coverage_limit = coverage_limit->Intersection(Rect::MakeSize(root_pass_size)); if (!coverage_limit.has_value()) { + capture.CreateChild("Subpass Entity (Skipped: Empty coverage limit B)"); return EntityPass::EntityResult::Skip(); } @@ -620,11 +639,13 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( ? coverage_limit : GetSubpassCoverage(*subpass, coverage_limit); if (!subpass_coverage.has_value()) { + capture.CreateChild("Subpass Entity (Skipped: Empty subpass coverage A)"); return EntityPass::EntityResult::Skip(); } auto subpass_size = ISize(subpass_coverage->GetSize()); if (subpass_size.IsEmpty()) { + capture.CreateChild("Subpass Entity (Skipped: Empty subpass coverage B)"); return EntityPass::EntityResult::Skip(); } @@ -639,6 +660,9 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( return EntityPass::EntityResult::Failure(); } + auto subpass_capture = capture.CreateChild("EntityPass"); + subpass_capture.AddRect("Coverage", *subpass_coverage, {.readonly = true}); + // Start non-collapsed subpasses with a fresh clip coverage stack limited by // the subpass coverage. This is important because image filters applied to // save layers may transform the subpass texture after it's rendered, @@ -650,6 +674,7 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( // time they are transient). if (!subpass->OnRender( renderer, // renderer + subpass_capture, // capture root_pass_size, // root_pass_size subpass_target, // pass_target subpass_coverage->GetOrigin(), // global_pass_position @@ -689,11 +714,16 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( return EntityPass::EntityResult::Failure(); } Entity element_entity; + Capture subpass_texture_capture = + capture.CreateChild("Entity (Subpass texture)"); element_entity.SetClipDepth(subpass->clip_depth_); + element_entity.SetCapture(subpass_texture_capture); element_entity.SetContents(std::move(offscreen_texture_contents)); element_entity.SetBlendMode(subpass->blend_mode_); - element_entity.SetTransform(Matrix::MakeTranslation( - Vector3(subpass_coverage->GetOrigin() - global_pass_position))); + element_entity.SetTransform(subpass_texture_capture.AddMatrix( + "Transform", + Matrix::MakeTranslation( + Vector3(subpass_coverage->GetOrigin() - global_pass_position)))); return EntityPass::EntityResult::Success(std::move(element_entity)); } @@ -816,6 +846,7 @@ bool EntityPass::RenderElement(Entity& element_entity, bool EntityPass::OnRender( ContentContext& renderer, + Capture& capture, ISize root_pass_size, EntityPassTarget& pass_target, Point global_pass_position, @@ -888,6 +919,7 @@ bool EntityPass::OnRender( EntityResult result = GetEntityForElement(element, // element renderer, // renderer + capture, // capture pass_context, // pass_context root_pass_size, // root_pass_size global_pass_position, // global_pass_position diff --git a/impeller/entity/entity_pass.h b/impeller/entity/entity_pass.h index 87731602d6925..4fcf7f6cf3b05 100644 --- a/impeller/entity/entity_pass.h +++ b/impeller/entity/entity_pass.h @@ -53,6 +53,8 @@ class EntityPass { /// `GetEntityForElement()`. using Element = std::variant>; + static const std::string kCaptureDocumentName; + using BackdropFilterProc = std::function( FilterInput::Ref, const Matrix& effect_transform, @@ -236,6 +238,7 @@ class EntityPass { EntityResult GetEntityForElement(const EntityPass::Element& element, ContentContext& renderer, + Capture& capture, InlinePassContext& pass_context, ISize root_pass_size, Point global_pass_position, @@ -301,6 +304,7 @@ class EntityPass { /// parent pass. /// bool OnRender(ContentContext& renderer, + Capture& capture, ISize root_pass_size, EntityPassTarget& pass_target, Point global_pass_position, diff --git a/impeller/entity/entity_pass_clip_stack.cc b/impeller/entity/entity_pass_clip_stack.cc index 54a30aceef209..68dcf2271eca5 100644 --- a/impeller/entity/entity_pass_clip_stack.cc +++ b/impeller/entity/entity_pass_clip_stack.cc @@ -124,6 +124,18 @@ EntityPassClipStack::ClipStateResult EntityPassClipStack::ApplyClipState( } break; } +#ifdef IMPELLER_ENABLE_CAPTURE + { + auto element_entity_coverage = entity.GetCoverage(); + if (element_entity_coverage.has_value()) { + element_entity_coverage = + element_entity_coverage->Shift(global_pass_position); + entity.GetCapture().AddRect("Coverage", *element_entity_coverage, + {.readonly = true}); + } + } +#endif + RecordEntity(entity, global_clip_coverage.type, subpass_state.clip_coverage.back().coverage); diff --git a/impeller/renderer/context.cc b/impeller/renderer/context.cc index b6d5a624cb97a..fb42d5744a10b 100644 --- a/impeller/renderer/context.cc +++ b/impeller/renderer/context.cc @@ -4,11 +4,13 @@ #include "impeller/renderer/context.h" +#include "impeller/core/capture.h" + namespace impeller { Context::~Context() = default; -Context::Context() = default; +Context::Context() : capture(CaptureContext::MakeInactive()) {} bool Context::UpdateOffscreenLayerPixelFormat(PixelFormat format) { return false; diff --git a/impeller/renderer/context.h b/impeller/renderer/context.h index 4a51560978bb2..59ac871aad483 100644 --- a/impeller/renderer/context.h +++ b/impeller/renderer/context.h @@ -9,6 +9,7 @@ #include #include "impeller/core/allocator.h" +#include "impeller/core/capture.h" #include "impeller/core/formats.h" #include "impeller/renderer/capabilities.h" #include "impeller/renderer/command_queue.h" @@ -170,6 +171,8 @@ class Context { /// virtual void Shutdown() = 0; + CaptureContext capture; + /// Stores a task on the `ContextMTL` that is awaiting access for the GPU. /// /// The task will be executed in the event that the GPU access has changed to diff --git a/impeller/tools/impeller.gni b/impeller/tools/impeller.gni index bfc3932fb05b4..55453fe574f32 100644 --- a/impeller/tools/impeller.gni +++ b/impeller/tools/impeller.gni @@ -11,6 +11,9 @@ declare_args() { impeller_debug = flutter_runtime_mode == "debug" || flutter_runtime_mode == "profile" + # Whether the runtime capture/playback system is enabled. + impeller_capture = flutter_runtime_mode == "debug" + # Whether the Metal backend is enabled. impeller_enable_metal = (is_mac || is_ios) && target_os != "fuchsia" diff --git a/shell/gpu/gpu_surface_gl_impeller.h b/shell/gpu/gpu_surface_gl_impeller.h index d35dd14e962f5..5b6fbd9c1f3b5 100644 --- a/shell/gpu/gpu_surface_gl_impeller.h +++ b/shell/gpu/gpu_surface_gl_impeller.h @@ -12,7 +12,6 @@ #include "flutter/impeller/aiks/aiks_context.h" #include "flutter/impeller/renderer/context.h" #include "flutter/shell/gpu/gpu_surface_gl_delegate.h" -#include "impeller/renderer/renderer.h" namespace flutter { diff --git a/shell/gpu/gpu_surface_vulkan_impeller.h b/shell/gpu/gpu_surface_vulkan_impeller.h index fb761329ee95a..3ddb106fa7c49 100644 --- a/shell/gpu/gpu_surface_vulkan_impeller.h +++ b/shell/gpu/gpu_surface_vulkan_impeller.h @@ -12,7 +12,6 @@ #include "flutter/impeller/aiks/aiks_context.h" #include "flutter/impeller/renderer/context.h" #include "flutter/shell/gpu/gpu_surface_vulkan_delegate.h" -#include "impeller/renderer/renderer.h" namespace flutter { diff --git a/testing/impeller_golden_tests_output.txt b/testing/impeller_golden_tests_output.txt index f267f1d5759c8..67fc0eb14d438 100644 --- a/testing/impeller_golden_tests_output.txt +++ b/testing/impeller_golden_tests_output.txt @@ -478,6 +478,9 @@ impeller_Play_AiksTest_CanRenderWithContiguousClipRestores_Vulkan.png impeller_Play_AiksTest_CanSaveLayerStandalone_Metal.png impeller_Play_AiksTest_CanSaveLayerStandalone_OpenGLES.png impeller_Play_AiksTest_CanSaveLayerStandalone_Vulkan.png +impeller_Play_AiksTest_CaptureContext_Metal.png +impeller_Play_AiksTest_CaptureContext_OpenGLES.png +impeller_Play_AiksTest_CaptureContext_Vulkan.png impeller_Play_AiksTest_ClearBlendWithBlur_Metal.png impeller_Play_AiksTest_ClearBlendWithBlur_OpenGLES.png impeller_Play_AiksTest_ClearBlendWithBlur_Vulkan.png From debe53f96966394dd765d1e86608c044c752c8db Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Wed, 8 May 2024 10:40:58 -0700 Subject: [PATCH 2/2] format --- impeller/aiks/aiks_unittests.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index a71ec0dad8e30..12c5414ae1b70 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -20,8 +20,8 @@ #include "impeller/aiks/image_filter.h" #include "impeller/aiks/paint_pass_delegate.h" #include "impeller/aiks/testing/context_spy.h" -#include "impeller/core/device_buffer.h" #include "impeller/core/capture.h" +#include "impeller/core/device_buffer.h" #include "impeller/entity/contents/solid_color_contents.h" #include "impeller/geometry/color.h" #include "impeller/geometry/constants.h"