From 1a753d8ca22fce0a2eaa4aae479212059abf9baf Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 23 Aug 2023 19:02:06 -0700 Subject: [PATCH] [Impeller] Add debug captures and inspector. (flutter/engine#43764) Weekend project! Press `C` to capture in the Aiks playground. Decided to finally give this a go and attempt to relieve some of the print debugging/mindfuck around investigation of coverage-related issues lately. :) Captures: * Capture documents from anywhere in Impeller. * Easily implement inspectors for those documents. * Replay documents with live editing. * No overhead when capturing is build time disabled (that's the idea, anyway). * Low overhead when capturing is runtime disabled. Aiks inspector: * Outline passes and rendered entities. * Identify collapsed passes. * Visibly highlight coverage. * Live edit scene properties. Possible future work: * Filters! * Blend mode property. * Pointer + release proc property. * Support captures in the DL playground. * Text atlas visualization. * Multi-frame capture and scrubbing. * Menus instead of key bindings? https://github.com/flutter/engine/assets/919017/a7a63e24-f72f-4140-a21e-6ca02a05fc20 --- .../ci/licenses_golden/licenses_flutter | 8 + engine/src/flutter/impeller/BUILD.gn | 5 +- engine/src/flutter/impeller/aiks/BUILD.gn | 2 + .../flutter/impeller/aiks/aiks_playground.cc | 22 +- .../flutter/impeller/aiks/aiks_playground.h | 6 +- .../aiks/aiks_playground_inspector.cc | 273 +++++++++++++++++ .../impeller/aiks/aiks_playground_inspector.h | 37 +++ .../flutter/impeller/aiks/aiks_unittests.cc | 75 +++-- engine/src/flutter/impeller/core/BUILD.gn | 2 + engine/src/flutter/impeller/core/capture.cc | 212 +++++++++++++ engine/src/flutter/impeller/core/capture.h | 290 ++++++++++++++++++ .../entity/contents/solid_color_contents.cc | 6 +- .../entity/contents/texture_contents.cc | 19 +- engine/src/flutter/impeller/entity/entity.cc | 8 + engine/src/flutter/impeller/entity/entity.h | 6 + .../flutter/impeller/entity/entity_pass.cc | 40 +++ .../src/flutter/impeller/entity/entity_pass.h | 18 ++ .../golden_tests/golden_playground_test.h | 6 +- .../golden_playground_test_mac.cc | 5 +- .../golden_playground_test_stub.cc | 7 +- .../src/flutter/impeller/renderer/context.h | 3 + 21 files changed, 995 insertions(+), 55 deletions(-) create mode 100644 engine/src/flutter/impeller/aiks/aiks_playground_inspector.cc create mode 100644 engine/src/flutter/impeller/aiks/aiks_playground_inspector.h create mode 100644 engine/src/flutter/impeller/core/capture.cc create mode 100644 engine/src/flutter/impeller/core/capture.h diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 8598c28bb7..a2b03147f7 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -1015,6 +1015,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/color_filter.cc + ../../../flutter/LICENSE @@ -1127,6 +1129,8 @@ ORIGIN: ../../../flutter/impeller/core/buffer.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/core/buffer.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 @@ -3753,6 +3757,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/color_filter.cc @@ -3865,6 +3871,8 @@ FILE: ../../../flutter/impeller/core/buffer.cc FILE: ../../../flutter/impeller/core/buffer.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/engine/src/flutter/impeller/BUILD.gn b/engine/src/flutter/impeller/BUILD.gn index 9855acc959..0217b3d4ea 100644 --- a/engine/src/flutter/impeller/BUILD.gn +++ b/engine/src/flutter/impeller/BUILD.gn @@ -11,7 +11,10 @@ config("impeller_public_config") { defines = [] if (impeller_debug) { - defines += [ "IMPELLER_DEBUG=1" ] + defines += [ + "IMPELLER_DEBUG=1", + "IMPELLER_ENABLE_CAPTURE=1", + ] } if (impeller_supports_rendering) { diff --git a/engine/src/flutter/impeller/aiks/BUILD.gn b/engine/src/flutter/impeller/aiks/BUILD.gn index 91ba4d190b..bfd814305d 100644 --- a/engine/src/flutter/impeller/aiks/BUILD.gn +++ b/engine/src/flutter/impeller/aiks/BUILD.gn @@ -41,6 +41,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/engine/src/flutter/impeller/aiks/aiks_playground.cc b/engine/src/flutter/impeller/aiks/aiks_playground.cc index d23a7315e2..3f31c658da 100644 --- a/engine/src/flutter/impeller/aiks/aiks_playground.cc +++ b/engine/src/flutter/impeller/aiks/aiks_playground.cc @@ -23,11 +23,10 @@ void AiksPlayground::SetTypographerContext( typographer_context_ = std::move(typographer_context); } -bool AiksPlayground::OpenPlaygroundHere(const Picture& picture) { - return OpenPlaygroundHere( - [&picture](AiksContext& renderer, RenderTarget& render_target) -> bool { - return renderer.Render(picture, render_target); - }); +bool AiksPlayground::OpenPlaygroundHere(Picture picture) { + return OpenPlaygroundHere([&picture](AiksContext& renderer) -> Picture { + return std::move(picture); + }); } bool AiksPlayground::OpenPlaygroundHere(AiksPlaygroundCallback callback) { @@ -42,13 +41,14 @@ bool AiksPlayground::OpenPlaygroundHere(AiksPlaygroundCallback callback) { } return Playground::OpenPlaygroundHere( - [&renderer, &callback](RenderTarget& render_target) -> bool { - static bool wireframe = false; - if (ImGui::IsKeyPressed(ImGuiKey_Z)) { - wireframe = !wireframe; - renderer.GetContentContext().SetWireframe(wireframe); + [this, &renderer, &callback](RenderTarget& render_target) -> bool { + const std::optional& picture = inspector_.RenderInspector( + renderer, [&]() { return callback(renderer); }); + + if (!picture.has_value()) { + return false; } - return callback(renderer, render_target); + return renderer.Render(*picture, render_target); }); } diff --git a/engine/src/flutter/impeller/aiks/aiks_playground.h b/engine/src/flutter/impeller/aiks/aiks_playground.h index 2d854b0c21..d33a15b07f 100644 --- a/engine/src/flutter/impeller/aiks/aiks_playground.h +++ b/engine/src/flutter/impeller/aiks/aiks_playground.h @@ -6,6 +6,7 @@ #include "flutter/fml/macros.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" @@ -15,7 +16,7 @@ namespace impeller { class AiksPlayground : public PlaygroundTest { public: using AiksPlaygroundCallback = - std::function; + std::function(AiksContext& renderer)>; AiksPlayground(); @@ -24,12 +25,13 @@ class AiksPlayground : public PlaygroundTest { void SetTypographerContext( std::shared_ptr typographer_context); - bool OpenPlaygroundHere(const Picture& picture); + bool OpenPlaygroundHere(Picture picture); bool OpenPlaygroundHere(AiksPlaygroundCallback callback); private: std::shared_ptr typographer_context_; + AiksInspector inspector_; FML_DISALLOW_COPY_AND_ASSIGN(AiksPlayground); }; diff --git a/engine/src/flutter/impeller/aiks/aiks_playground_inspector.cc b/engine/src/flutter/impeller/aiks/aiks_playground_inspector.cc new file mode 100644 index 0000000000..0f8721358d --- /dev/null +++ b/engine/src/flutter/impeller/aiks/aiks_playground_inspector.cc @@ -0,0 +1,273 @@ +// 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_; +} + +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/engine/src/flutter/impeller/aiks/aiks_playground_inspector.h b/engine/src/flutter/impeller/aiks/aiks_playground_inspector.h new file mode 100644 index 0000000000..95b1e07778 --- /dev/null +++ b/engine/src/flutter/impeller/aiks/aiks_playground_inspector.h @@ -0,0 +1,37 @@ +// 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 +#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); + + 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_; + + FML_DISALLOW_COPY_AND_ASSIGN(AiksInspector); +}; + +}; // namespace impeller diff --git a/engine/src/flutter/impeller/aiks/aiks_unittests.cc b/engine/src/flutter/impeller/aiks/aiks_unittests.cc index ba4305c1b6..3143fc14f1 100644 --- a/engine/src/flutter/impeller/aiks/aiks_unittests.cc +++ b/engine/src/flutter/impeller/aiks/aiks_unittests.cc @@ -17,6 +17,7 @@ #include "impeller/aiks/image.h" #include "impeller/aiks/paint_pass_delegate.h" #include "impeller/aiks/testing/context_spy.h" +#include "impeller/core/capture.h" #include "impeller/entity/contents/color_source_contents.h" #include "impeller/entity/contents/conical_gradient_contents.h" #include "impeller/entity/contents/filters/inputs/filter_input.h" @@ -641,7 +642,7 @@ TEST_P(AiksTest, CanRenderLinearGradientWayManyColorsClamp) { } TEST_P(AiksTest, CanRenderLinearGradientManyColorsUnevenStops) { - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; const Entity::TileMode tile_modes[] = { Entity::TileMode::kClamp, Entity::TileMode::kRepeat, @@ -686,7 +687,7 @@ TEST_P(AiksTest, CanRenderLinearGradientManyColorsUnevenStops) { {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); canvas.DrawRect({0, 0, 600, 600}, paint); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); } @@ -717,7 +718,7 @@ TEST_P(AiksTest, CanRenderLinearGradientMaskBlur) { } TEST_P(AiksTest, CanRenderRadialGradient) { - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; const Entity::TileMode tile_modes[] = { Entity::TileMode::kClamp, Entity::TileMode::kRepeat, @@ -754,13 +755,13 @@ TEST_P(AiksTest, CanRenderRadialGradient) { {100, 100}, 100, std::move(colors), std::move(stops), tile_mode, {}); canvas.DrawRect({0, 0, 600, 600}, paint); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); } TEST_P(AiksTest, CanRenderRadialGradientManyColors) { - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; const Entity::TileMode tile_modes[] = { Entity::TileMode::kClamp, Entity::TileMode::kRepeat, @@ -811,7 +812,7 @@ TEST_P(AiksTest, CanRenderRadialGradientManyColors) { {100, 100}, 100, std::move(colors), std::move(stops), tile_mode, {}); canvas.DrawRect({0, 0, 600, 600}, paint); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); } @@ -1341,8 +1342,7 @@ TEST_P(AiksTest, TextFrameSubpixelAlignment) { offset = (static_cast(rand) / static_cast(RAND_MAX)) * k2Pi; } - auto callback = [&](AiksContext& renderer, - RenderTarget& render_target) -> bool { + auto callback = [&](AiksContext& renderer) -> std::optional { static float font_size = 20; static float phase_variation = 0.2; static float speed = 0.5; @@ -1369,10 +1369,10 @@ TEST_P(AiksTest, TextFrameSubpixelAlignment) { "the lazy dog!.?", "Roboto-Regular.ttf", {.font_size = font_size, .position = position})) { - return false; + return std::nullopt; } } - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); @@ -1543,7 +1543,7 @@ static BlendModeSelection GetBlendModeSelection() { TEST_P(AiksTest, CanDrawPaintMultipleTimesInteractive) { auto modes = GetBlendModeSelection(); - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { static Color background = Color::MediumTurquoise(); static Color foreground = Color::Color::OrangeRed().WithAlpha(0.5); static int current_blend_index = 3; @@ -1564,7 +1564,7 @@ TEST_P(AiksTest, CanDrawPaintMultipleTimesInteractive) { canvas.DrawPaint( {.color = foreground, .blend_mode = static_cast(current_blend_index)}); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); } @@ -1633,7 +1633,7 @@ TEST_P(AiksTest, ColorWheel) { std::shared_ptr color_wheel_image; Matrix color_wheel_transform; - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { // UI state. static bool cache_the_wheel = true; static int current_blend_index = 3; @@ -1675,7 +1675,7 @@ TEST_P(AiksTest, ColorWheel) { auto color_wheel_picture = canvas.EndRecordingAsPicture(); auto snapshot = color_wheel_picture.Snapshot(renderer); if (!snapshot.has_value() || !snapshot->texture) { - return false; + return std::nullopt; } color_wheel_image = std::make_shared(snapshot->texture); color_wheel_transform = snapshot->transform; @@ -1718,7 +1718,7 @@ TEST_P(AiksTest, ColorWheel) { } canvas.Restore(); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); @@ -1765,7 +1765,7 @@ TEST_P(AiksTest, TransformMultipliesCorrectly) { TEST_P(AiksTest, SolidStrokesRenderCorrectly) { // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { static Color color = Color::Black().WithAlpha(0.5); static float scale = 3; static bool add_circle_clip = true; @@ -1820,7 +1820,7 @@ TEST_P(AiksTest, SolidStrokesRenderCorrectly) { canvas.Translate({-240, 60}); } - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); @@ -1828,7 +1828,7 @@ TEST_P(AiksTest, SolidStrokesRenderCorrectly) { TEST_P(AiksTest, GradientStrokesRenderCorrectly) { // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { static float scale = 3; static bool add_circle_clip = true; const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; @@ -1897,14 +1897,14 @@ TEST_P(AiksTest, GradientStrokesRenderCorrectly) { canvas.Translate({-240, 60}); } - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); } TEST_P(AiksTest, CoverageOriginShouldBeAccountedForInSubpasses) { - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { Canvas canvas; canvas.Scale(GetContentScale()); @@ -1931,7 +1931,7 @@ TEST_P(AiksTest, CoverageOriginShouldBeAccountedForInSubpasses) { canvas.Restore(); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); @@ -2066,7 +2066,7 @@ TEST_P(AiksTest, SceneColorSource) { *mapping, *GetContext()->GetResourceAllocator()); ASSERT_NE(gltf_scene, nullptr); - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { Paint paint; ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); @@ -2091,7 +2091,7 @@ TEST_P(AiksTest, SceneColorSource) { canvas.DrawPaint(Paint{.color = Color::MakeRGBA8(0xf9, 0xf9, 0xf9, 0xff)}); canvas.Scale(GetContentScale()); canvas.DrawPaint(paint); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); @@ -2746,7 +2746,7 @@ TEST_P(AiksTest, CanRenderMaskBlurHugeSigma) { } TEST_P(AiksTest, CanRenderBackdropBlurInteractive) { - auto callback = [&](AiksContext& renderer, RenderTarget& render_target) { + auto callback = [&](AiksContext& renderer) -> std::optional { auto [a, b] = IMPELLER_PLAYGROUND_LINE(Point(50, 50), Point(300, 200), 30, Color::White(), Color::White()); @@ -2766,7 +2766,7 @@ TEST_P(AiksTest, CanRenderBackdropBlurInteractive) { }); canvas.Restore(); - return renderer.Render(canvas.EndRecordingAsPicture(), render_target); + return canvas.EndRecordingAsPicture(); }; ASSERT_TRUE(OpenPlaygroundHere(callback)); @@ -3271,5 +3271,30 @@ 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}); + + ImGui::Begin("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); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/core/BUILD.gn b/engine/src/flutter/impeller/core/BUILD.gn index cc66990c90..36c7ddb550 100644 --- a/engine/src/flutter/impeller/core/BUILD.gn +++ b/engine/src/flutter/impeller/core/BUILD.gn @@ -12,6 +12,8 @@ impeller_component("core") { "buffer.h", "buffer_view.cc", "buffer_view.h", + "capture.cc", + "capture.h", "device_buffer.cc", "device_buffer.h", "device_buffer_descriptor.cc", diff --git a/engine/src/flutter/impeller/core/capture.cc b/engine/src/flutter/impeller/core/capture.cc new file mode 100644 index 0000000000..bf5a2cb6b0 --- /dev/null +++ b/engine/src/flutter/impeller/core/capture.cc @@ -0,0 +1,212 @@ +// 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(const std::string& label, \ + type_name value, \ + CaptureProperty::Options options) { \ + if (!active_) { \ + return value; \ + } \ + FML_DCHECK(element_ != nullptr); \ + \ + auto new_value = Capture##pascal_name##Property::Make( \ + label, 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); +} + +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/engine/src/flutter/impeller/core/capture.h b/engine/src/flutter/impeller/core/capture.h new file mode 100644 index 0000000000..3e21c71e61 --- /dev/null +++ b/engine/src/flutter/impeller/core/capture.h @@ -0,0 +1,290 @@ +// 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. + +#pragma once + +#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_; + + FML_DISALLOW_COPY_AND_ASSIGN(CapturePlaybackList); +}; + +/// 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); + + FML_DISALLOW_COPY_AND_ASSIGN(CaptureElement); +}; + +#ifdef IMPELLER_ENABLE_CAPTURE +#define _CAPTURE_PROPERTY_RECORDER_DECLARATION(type_name, pascal_name, \ + lower_name) \ + type_name Add##pascal_name(const std::string& 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(const std::string& 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(const std::string& label) { +#ifdef IMPELLER_ENABLE_CAPTURE + if (!active_) { + return Capture(); + } + + auto new_capture = Capture(label); + 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); + + 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 diff --git a/engine/src/flutter/impeller/entity/contents/solid_color_contents.cc b/engine/src/flutter/impeller/entity/contents/solid_color_contents.cc index 47ea391b84..d42f9f8fa0 100644 --- a/engine/src/flutter/impeller/entity/contents/solid_color_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/solid_color_contents.cc @@ -44,6 +44,8 @@ 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; Command cmd; @@ -64,8 +66,8 @@ bool SolidColorContents::Render(const ContentContext& renderer, cmd.BindVertices(geometry_result.vertex_buffer); VS::FrameInfo frame_info; - frame_info.mvp = geometry_result.transform; - frame_info.color = GetColor().Premultiply(); + frame_info.mvp = capture.AddMatrix("Transform", geometry_result.transform); + frame_info.color = capture.AddColor("Color", GetColor()).Premultiply(); VS::BindFrameInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(frame_info)); if (!pass.AddCommand(std::move(cmd))) { diff --git a/engine/src/flutter/impeller/entity/contents/texture_contents.cc b/engine/src/flutter/impeller/entity/contents/texture_contents.cc index e224e74eea..48fceefb36 100644 --- a/engine/src/flutter/impeller/entity/contents/texture_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/texture_contents.cc @@ -108,6 +108,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 FSExternal = TextureFillExternalFragmentShader; @@ -123,24 +125,27 @@ bool TextureContents::Render(const ContentContext& renderer, // Expand the source rect by half a texel, which aligns sampled texels to the // pixel grid if the source rect is the same size as the destination rect. auto texture_coords = - Rect::MakeSize(texture_->GetSize()).Project(source_rect_.Expand(0.5)); + Rect::MakeSize(texture_->GetSize()) + .Project(capture.AddRect("Source rect", source_rect_).Expand(0.5)); 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 = pass.GetTransientsBuffer(); VS::FrameInfo frame_info; frame_info.mvp = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * - entity.GetTransformation(); + capture.AddMatrix("Transform", entity.GetTransformation()); frame_info.texture_sampler_y_coord_scale = texture_->GetYCoordScale(); - frame_info.alpha = GetOpacity(); + frame_info.alpha = capture.AddScalar("Alpha", GetOpacity()); Command cmd; if (label_.empty()) { diff --git a/engine/src/flutter/impeller/entity/entity.cc b/engine/src/flutter/impeller/entity/entity.cc index ad6a6842d6..b7ebc2af96 100644 --- a/engine/src/flutter/impeller/entity/entity.cc +++ b/engine/src/flutter/impeller/entity/entity.cc @@ -170,4 +170,12 @@ Scalar Entity::DeriveTextScale() const { return GetTransformation().GetMaxBasisLengthXY(); } +Capture& Entity::GetCapture() const { + return capture_; +} + +void Entity::SetCapture(Capture capture) const { + capture_ = std::move(capture); +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/entity/entity.h b/engine/src/flutter/impeller/entity/entity.h index 271f9a0db8..929824fbf5 100644 --- a/engine/src/flutter/impeller/entity/entity.h +++ b/engine/src/flutter/impeller/entity/entity.h @@ -5,6 +5,7 @@ #pragma once #include +#include "impeller/core/capture.h" #include "impeller/entity/contents/contents.h" #include "impeller/geometry/color.h" #include "impeller/geometry/matrix.h" @@ -96,11 +97,16 @@ class Entity { Scalar DeriveTextScale() const; + Capture& GetCapture() const; + + void SetCapture(Capture capture) const; + private: Matrix transformation_; std::shared_ptr contents_; BlendMode blend_mode_ = BlendMode::kSourceOver; uint32_t stencil_depth_ = 0u; + mutable Capture capture_; }; } // namespace impeller diff --git a/engine/src/flutter/impeller/entity/entity_pass.cc b/engine/src/flutter/impeller/entity/entity_pass.cc index b0a4b883fb..f770165215 100644 --- a/engine/src/flutter/impeller/entity/entity_pass.cc +++ b/engine/src/flutter/impeller/entity/entity_pass.cc @@ -53,6 +53,8 @@ std::tuple, BlendMode> ElementAsBackgroundColor( } } // namespace +const std::string EntityPass::kCaptureDocumentName = "EntityPass"; + EntityPass::EntityPass() = default; EntityPass::~EntityPass() = default; @@ -250,7 +252,11 @@ uint32_t EntityPass::GetTotalPassReads(ContentContext& renderer) const { bool EntityPass::Render(ContentContext& renderer, const RenderTarget& render_target) const { + auto capture = + renderer.GetContext()->capture.GetDocument(kCaptureDocumentName); + renderer.GetRenderTargetCache()->Start(); + auto root_render_target = render_target; if (root_render_target.GetColorAttachments().find(0u) == @@ -259,6 +265,10 @@ bool EntityPass::Render(ContentContext& renderer, return false; } + capture.AddRect("Coverage", + Rect::MakeSize(root_render_target.GetRenderTargetSize()), + {.readonly = true}); + fml::ScopedCleanupClosure reset_state([&renderer]() { renderer.GetLazyGlyphAtlas()->ResetTextFrames(); renderer.GetRenderTargetCache()->End(); @@ -291,6 +301,7 @@ bool EntityPass::Render(ContentContext& renderer, GetClearColor(render_target.GetRenderTargetSize())); if (!OnRender(renderer, // renderer + capture, // capture offscreen_target.GetRenderTarget() .GetRenderTargetSize(), // root_pass_size offscreen_target, // pass_target @@ -403,6 +414,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 @@ -414,6 +426,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, @@ -428,6 +441,7 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( if (const auto& entity = std::get_if(&element)) { element_entity = *entity; + 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, // apply the negative translation to entity copies before rendering them @@ -452,9 +466,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 @@ -490,10 +506,12 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( if (stencil_coverage_stack.empty()) { // 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 stencil_coverage_back = stencil_coverage_stack.back().coverage; if (!stencil_coverage_back.has_value()) { + capture.CreateChild("Subpass Entity (Skipped: Empty clip B)"); return EntityPass::EntityResult::Skip(); } @@ -505,12 +523,14 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( .GetRenderTargetSize())) .Intersection(stencil_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(); } @@ -519,11 +539,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->size); if (subpass_size.IsEmpty()) { + capture.CreateChild("Subpass Entity (Skipped: Empty subpass coverage B)"); return EntityPass::EntityResult::Skip(); } @@ -538,10 +560,14 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( return EntityPass::EntityResult::Failure(); } + auto subpass_capture = capture.CreateChild("EntityPass"); + subpass_capture.AddRect("Coverage", *subpass_coverage, {.readonly = true}); + // Stencil textures aren't shared between EntityPasses (as much of the // time they are transient). if (!subpass->OnRender( renderer, // renderer + subpass_capture, // capture root_pass_size, // root_pass_size subpass_target, // pass_target subpass_coverage->origin, // global_pass_position @@ -579,6 +605,7 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( return EntityPass::EntityResult::Failure(); } + element_entity.SetCapture(capture.CreateChild("Entity (Subpass texture)")); element_entity.SetContents(std::move(offscreen_texture_contents)); element_entity.SetStencilDepth(subpass->stencil_depth_); element_entity.SetBlendMode(subpass->blend_mode_); @@ -593,6 +620,7 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( bool EntityPass::OnRender( ContentContext& renderer, + Capture& capture, ISize root_pass_size, EntityPassTarget& pass_target, Point global_pass_position, @@ -728,6 +756,17 @@ bool EntityPass::OnRender( } break; } +#ifdef IMPELLER_ENABLE_CAPTURE + { + auto element_entity_coverage = element_entity.GetCoverage(); + if (element_entity_coverage.has_value()) { + element_entity_coverage->origin += global_pass_position; + element_entity.GetCapture().AddRect( + "Coverage", *element_entity_coverage, {.readonly = true}); + } + } +#endif + element_entity.SetStencilDepth(element_entity.GetStencilDepth() - stencil_depth_floor); if (!element_entity.Render(renderer, *result.pass)) { @@ -774,6 +813,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/engine/src/flutter/impeller/entity/entity_pass.h b/engine/src/flutter/impeller/entity/entity_pass.h index d2dcb06018..31db5d2324 100644 --- a/engine/src/flutter/impeller/entity/entity_pass.h +++ b/engine/src/flutter/impeller/entity/entity_pass.h @@ -36,6 +36,8 @@ class EntityPass { /// `GetEntityForElement()`. using Element = std::variant>; + static const std::string kCaptureDocumentName; + using BackdropFilterProc = std::function( FilterInput::Ref, const Matrix& effect_transform, @@ -75,12 +77,16 @@ class EntityPass { void SetElements(std::vector elements); + //---------------------------------------------------------------------------- /// @brief Appends a given pass as a subpass. + /// EntityPass* AddSubpass(std::unique_ptr pass); + //---------------------------------------------------------------------------- /// @brief Merges a given pass into this pass. Useful for drawing /// pre-recorded pictures that don't require rendering into a separate /// subpass. + /// void AddSubpassInline(std::unique_ptr pass); EntityPass* GetSuperpass() const; @@ -94,23 +100,31 @@ class EntityPass { /// it's included in the stream before its children. void IterateAllElements(const std::function& iterator); + //---------------------------------------------------------------------------- /// @brief Iterate all entities in this pass, recursively including entities /// of child passes. The iteration order is depth-first. + /// void IterateAllEntities(const std::function& iterator); + //---------------------------------------------------------------------------- /// @brief Iterate all entities in this pass, recursively including entities /// of child passes. The iteration order is depth-first and does not /// allow modification of the entities. + /// void IterateAllEntities( const std::function& iterator) const; + //---------------------------------------------------------------------------- /// @brief Iterate entities in this pass up until the first subpass is found. /// This is useful for limiting look-ahead optimizations. /// /// @return Returns whether a subpass was encountered. + /// bool IterateUntilSubpass(const std::function& iterator); + //---------------------------------------------------------------------------- /// @brief Return the number of elements on this pass. + /// size_t GetElementCount() const; void SetTransformation(Matrix xformation); @@ -160,6 +174,7 @@ class EntityPass { EntityResult GetEntityForElement(const EntityPass::Element& element, ContentContext& renderer, + Capture& capture, InlinePassContext& pass_context, ISize root_pass_size, Point global_pass_position, @@ -167,6 +182,7 @@ class EntityPass { StencilCoverageStack& stencil_coverage_stack, size_t stencil_depth_floor) const; + //---------------------------------------------------------------------------- /// @brief OnRender is the internal command recording routine for /// `EntityPass`. Its job is to walk through each `Element` which /// was appended to the scene (either an `Entity` via `AddEntity()` @@ -222,7 +238,9 @@ class EntityPass { /// creating a new `RenderPass`. This /// "collapses" the Elements into the /// parent pass. + /// bool OnRender(ContentContext& renderer, + Capture& capture, ISize root_pass_size, EntityPassTarget& pass_target, Point global_pass_position, diff --git a/engine/src/flutter/impeller/golden_tests/golden_playground_test.h b/engine/src/flutter/impeller/golden_tests/golden_playground_test.h index 955d39f4b9..9754d943be 100644 --- a/engine/src/flutter/impeller/golden_tests/golden_playground_test.h +++ b/engine/src/flutter/impeller/golden_tests/golden_playground_test.h @@ -23,7 +23,7 @@ class GoldenPlaygroundTest : public ::testing::TestWithParam { public: using AiksPlaygroundCallback = - std::function; + std::function(AiksContext& renderer)>; GoldenPlaygroundTest(); @@ -38,9 +38,9 @@ class GoldenPlaygroundTest void SetTypographerContext( std::shared_ptr typographer_context); - bool OpenPlaygroundHere(const Picture& picture); + bool OpenPlaygroundHere(Picture picture); - bool OpenPlaygroundHere(const AiksPlaygroundCallback& callback); + bool OpenPlaygroundHere(AiksPlaygroundCallback callback); std::shared_ptr CreateTextureForFixture( const char* fixture_name, diff --git a/engine/src/flutter/impeller/golden_tests/golden_playground_test_mac.cc b/engine/src/flutter/impeller/golden_tests/golden_playground_test_mac.cc index 574caad3ea..79fafd9361 100644 --- a/engine/src/flutter/impeller/golden_tests/golden_playground_test_mac.cc +++ b/engine/src/flutter/impeller/golden_tests/golden_playground_test_mac.cc @@ -134,7 +134,7 @@ PlaygroundBackend GoldenPlaygroundTest::GetBackend() const { return GetParam(); } -bool GoldenPlaygroundTest::OpenPlaygroundHere(const Picture& picture) { +bool GoldenPlaygroundTest::OpenPlaygroundHere(Picture picture) { AiksContext renderer(GetContext(), typographer_context_); auto screenshot = pimpl_->screenshoter->MakeScreenshot(renderer, picture, @@ -143,7 +143,8 @@ bool GoldenPlaygroundTest::OpenPlaygroundHere(const Picture& picture) { } bool GoldenPlaygroundTest::OpenPlaygroundHere( - const AiksPlaygroundCallback& callback) { + AiksPlaygroundCallback + callback) { // NOLINT(performance-unnecessary-value-param) return false; } diff --git a/engine/src/flutter/impeller/golden_tests/golden_playground_test_stub.cc b/engine/src/flutter/impeller/golden_tests/golden_playground_test_stub.cc index 10805238f5..fe7a931c11 100644 --- a/engine/src/flutter/impeller/golden_tests/golden_playground_test_stub.cc +++ b/engine/src/flutter/impeller/golden_tests/golden_playground_test_stub.cc @@ -4,6 +4,8 @@ #include "flutter/impeller/golden_tests/golden_playground_test.h" +#include "impeller/aiks/picture.h" + namespace impeller { GoldenPlaygroundTest::GoldenPlaygroundTest() = default; @@ -25,12 +27,13 @@ PlaygroundBackend GoldenPlaygroundTest::GetBackend() const { return GetParam(); } -bool GoldenPlaygroundTest::OpenPlaygroundHere(const Picture& picture) { +bool GoldenPlaygroundTest::OpenPlaygroundHere(Picture picture) { return false; } bool GoldenPlaygroundTest::OpenPlaygroundHere( - const AiksPlaygroundCallback& callback) { + AiksPlaygroundCallback + callback) { // NOLINT(performance-unnecessary-value-param) return false; } diff --git a/engine/src/flutter/impeller/renderer/context.h b/engine/src/flutter/impeller/renderer/context.h index b597e4fee1..1eb2662e6c 100644 --- a/engine/src/flutter/impeller/renderer/context.h +++ b/engine/src/flutter/impeller/renderer/context.h @@ -8,6 +8,7 @@ #include #include "flutter/fml/macros.h" +#include "impeller/core/capture.h" #include "impeller/core/formats.h" #include "impeller/core/host_buffer.h" #include "impeller/renderer/capabilities.h" @@ -164,6 +165,8 @@ class Context { /// @brief Accessor for a pool of HostBuffers. Pool& GetHostBufferPool() const { return host_buffer_pool_; } + CaptureContext capture; + protected: Context();