[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
This commit is contained in:
Brandon DeRosier
2023-08-23 19:02:06 -07:00
committed by GitHub
parent 7d82b4742d
commit 1a753d8ca2
21 changed files with 995 additions and 55 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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>& picture = inspector_.RenderInspector(
renderer, [&]() { return callback(renderer); });
if (!picture.has_value()) {
return false;
}
return callback(renderer, render_target);
return renderer.Render(*picture, render_target);
});
}

View File

@@ -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<bool(AiksContext& renderer, RenderTarget& render_target)>;
std::function<std::optional<Picture>(AiksContext& renderer)>;
AiksPlayground();
@@ -24,12 +25,13 @@ class AiksPlayground : public PlaygroundTest {
void SetTypographerContext(
std::shared_ptr<TypographerContext> typographer_context);
bool OpenPlaygroundHere(const Picture& picture);
bool OpenPlaygroundHere(Picture picture);
bool OpenPlaygroundHere(AiksPlaygroundCallback callback);
private:
std::shared_ptr<TypographerContext> typographer_context_;
AiksInspector inspector_;
FML_DISALLOW_COPY_AND_ASSIGN(AiksPlayground);
};

View File

@@ -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 <initializer_list>
#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<std::string> kSupportedDocuments = {
EntityPass::kCaptureDocumentName};
AiksInspector::AiksInspector() = default;
const std::optional<Picture>& AiksInspector::RenderInspector(
AiksContext& aiks_context,
const std::function<std::optional<Picture>()>& 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<Picture> 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<int>(p.options.range->min),
static_cast<int>(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<float*>(&p.value),
p.options.range->min, p.options.range->max);
return;
}
ImGui::DragFloat2(p.label.c_str(), reinterpret_cast<float*>(&p.value),
0.01);
},
.vector3 =
[](CaptureVector3Property& p) {
if (p.options.range.has_value()) {
ImGui::SliderFloat3(p.label.c_str(),
reinterpret_cast<float*>(&p.value),
p.options.range->min, p.options.range->max);
return;
}
ImGui::DragFloat3(p.label.c_str(), reinterpret_cast<float*>(&p.value),
0.01);
},
.rect =
[](CaptureRectProperty& p) {
ImGui::DragFloat4(p.label.c_str(), reinterpret_cast<float*>(&p.value),
0.01);
},
.color =
[](CaptureColorProperty& p) {
ImGui::ColorEdit4(p.label.c_str(),
reinterpret_cast<float*>(&p.value));
},
.matrix =
[](CaptureMatrixProperty& p) {
float* pointer = reinterpret_cast<float*>(&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<char*>(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

View File

@@ -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 <functional>
#include <optional>
#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<Picture>& RenderInspector(
AiksContext& aiks_context,
const std::function<std::optional<Picture>()>& 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<Picture> last_picture_;
FML_DISALLOW_COPY_AND_ASSIGN(AiksInspector);
};
}; // namespace impeller

View File

@@ -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<Picture> {
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<Picture> {
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<Picture> {
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<float>(rand) / static_cast<float>(RAND_MAX)) * k2Pi;
}
auto callback = [&](AiksContext& renderer,
RenderTarget& render_target) -> bool {
auto callback = [&](AiksContext& renderer) -> std::optional<Picture> {
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<Picture> {
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<BlendMode>(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<Image> color_wheel_image;
Matrix color_wheel_transform;
auto callback = [&](AiksContext& renderer, RenderTarget& render_target) {
auto callback = [&](AiksContext& renderer) -> std::optional<Picture> {
// 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<Image>(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<Picture> {
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<Picture> {
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<Picture> {
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<Picture> {
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<Picture> {
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<Picture> {
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<float*>(&p.value));
}});
});
ImGui::End();
return canvas.EndRecordingAsPicture();
};
OpenPlaygroundHere(callback);
}
} // namespace testing
} // namespace impeller

View File

@@ -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",

View File

@@ -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 <initializer_list>
#include <memory>
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<type_name> CaptureProperty::As##pascal_name() const { \
if (GetType() != Type::k##pascal_name) { \
return std::nullopt; \
} \
return reinterpret_cast<const Capture##pascal_name##Property*>(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> \
Capture##pascal_name##Property::Make(const std::string& label, \
type_name value, Options options) { \
auto result = std::shared_ptr<Capture##pascal_name##Property>( \
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> CaptureElement::Make(const std::string& label) {
return std::shared_ptr<CaptureElement>(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<CaptureElement> 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<Capture##pascal_name##Property>( \
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<std::string> allowlist)
: active_(true), allowlist_(allowlist) {}
#else
CaptureContext::CaptureContext() {}
CaptureContext::CaptureContext(std::initializer_list<std::string> allowlist) {}
#endif
CaptureContext::CaptureContext(CaptureContext::InactiveFlag) {}
CaptureContext CaptureContext::MakeInactive() {
return CaptureContext(InactiveFlag{});
}
CaptureContext CaptureContext::MakeAllowlist(
std::initializer_list<std::string> 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

View File

@@ -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 <functional>
#include <initializer_list>
#include <memory>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#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 <typename Type>
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<type_name> As##pascal_name() const;
/// A capturable property type
struct CaptureProperty : public CaptureCursorListElement<CaptureProperty> {
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> 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<Capture##pascal_name##Property> \
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<void(Capture##pascal_name##Property&)> lower_name = \
[](Capture##pascal_name##Property& value) {};
struct CaptureProcTable {
_FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROC)
};
template <typename Type>
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<CaptureElement>` property, and so the
// compiler fails the check due to the type being incomplete.
static_assert(std::is_base_of_v<CaptureCursorListElement<Type>, Type>);
}
void Rewind() { cursor_ = 0; }
size_t Count() { return values_.size(); }
std::shared_ptr<Type> GetNext(std::shared_ptr<Type> captured,
bool force_overwrite) {
if (cursor_ < values_.size()) {
std::shared_ptr<Type>& 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<Type> FindFirstByLabel(const std::string& label) {
for (std::shared_ptr<Type>& value : values_) {
if (value->label == label) {
return value;
}
}
return nullptr;
}
void Iterate(std::function<void(Type&)> iterator) const {
for (auto& value : values_) {
iterator(*value);
}
}
private:
size_t cursor_ = 0;
std::vector<std::shared_ptr<Type>> 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<CaptureElement> {
CapturePlaybackList<CaptureProperty> properties;
CapturePlaybackList<CaptureElement> children;
static std::shared_ptr<CaptureElement> 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<CaptureElement> GetElement() const;
void Rewind();
_FOR_EACH_CAPTURE_PROPERTY(_CAPTURE_PROPERTY_RECORDER_DECLARATION)
private:
#ifdef IMPELLER_ENABLE_CAPTURE
std::shared_ptr<CaptureElement> element_;
bool active_ = false;
#endif
};
class CaptureContext {
public:
CaptureContext();
static CaptureContext MakeInactive();
static CaptureContext MakeAllowlist(
std::initializer_list<std::string> 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<std::string> allowlist);
#ifdef IMPELLER_ENABLE_CAPTURE
bool active_ = false;
std::optional<std::unordered_set<std::string>> allowlist_;
std::unordered_map<std::string, Capture> documents_;
#endif
};
} // namespace impeller

View File

@@ -44,6 +44,8 @@ std::optional<Rect> 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))) {

View File

@@ -108,6 +108,8 @@ std::optional<Snapshot> 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<VS::PerVertexData> 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()) {

View File

@@ -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

View File

@@ -5,6 +5,7 @@
#pragma once
#include <variant>
#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> contents_;
BlendMode blend_mode_ = BlendMode::kSourceOver;
uint32_t stencil_depth_ = 0u;
mutable Capture capture_;
};
} // namespace impeller

View File

@@ -53,6 +53,8 @@ std::tuple<std::optional<Color>, 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<Entity>(&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

View File

@@ -36,6 +36,8 @@ class EntityPass {
/// `GetEntityForElement()`.
using Element = std::variant<Entity, std::unique_ptr<EntityPass>>;
static const std::string kCaptureDocumentName;
using BackdropFilterProc = std::function<std::shared_ptr<FilterContents>(
FilterInput::Ref,
const Matrix& effect_transform,
@@ -75,12 +77,16 @@ class EntityPass {
void SetElements(std::vector<Element> elements);
//----------------------------------------------------------------------------
/// @brief Appends a given pass as a subpass.
///
EntityPass* AddSubpass(std::unique_ptr<EntityPass> 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<EntityPass> pass);
EntityPass* GetSuperpass() const;
@@ -94,23 +100,31 @@ class EntityPass {
/// it's included in the stream before its children.
void IterateAllElements(const std::function<bool(Element&)>& 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<bool(Entity&)>& 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<bool(const Entity&)>& 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<bool(Entity&)>& 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,

View File

@@ -23,7 +23,7 @@ class GoldenPlaygroundTest
: public ::testing::TestWithParam<PlaygroundBackend> {
public:
using AiksPlaygroundCallback =
std::function<bool(AiksContext& renderer, RenderTarget& render_target)>;
std::function<std::optional<Picture>(AiksContext& renderer)>;
GoldenPlaygroundTest();
@@ -38,9 +38,9 @@ class GoldenPlaygroundTest
void SetTypographerContext(
std::shared_ptr<TypographerContext> typographer_context);
bool OpenPlaygroundHere(const Picture& picture);
bool OpenPlaygroundHere(Picture picture);
bool OpenPlaygroundHere(const AiksPlaygroundCallback& callback);
bool OpenPlaygroundHere(AiksPlaygroundCallback callback);
std::shared_ptr<Texture> CreateTextureForFixture(
const char* fixture_name,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -8,6 +8,7 @@
#include <string>
#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<HostBuffer>& GetHostBufferPool() const { return host_buffer_pool_; }
CaptureContext capture;
protected:
Context();