blob: 25167d5f6fb9a970f507295b8a30262896cbc290 [file] [log] [blame] [edit]
// 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 "flutter/impeller/aiks/aiks_unittests.h"
#include <array>
#include <cmath>
#include <cstdlib>
#include <memory>
#include <tuple>
#include <utility>
#include <vector>
#include "flutter/testing/testing.h"
#include "gtest/gtest.h"
#include "impeller/aiks/canvas.h"
#include "impeller/aiks/color_filter.h"
#include "impeller/aiks/image_filter.h"
#include "impeller/aiks/testing/context_spy.h"
#include "impeller/core/device_buffer.h"
#include "impeller/entity/contents/solid_color_contents.h"
#include "impeller/geometry/color.h"
#include "impeller/geometry/constants.h"
#include "impeller/geometry/geometry_asserts.h"
#include "impeller/geometry/matrix.h"
#include "impeller/geometry/path.h"
#include "impeller/geometry/path_builder.h"
#include "impeller/geometry/rect.h"
#include "impeller/geometry/size.h"
#include "impeller/playground/widgets.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/renderer/snapshot.h"
#include "third_party/imgui/imgui.h"
namespace impeller {
namespace testing {
INSTANTIATE_PLAYGROUND_SUITE(AiksTest);
#if IMPELLER_ENABLE_3D
TEST_P(AiksTest, SceneColorSource) {
// Load up the scene.
auto mapping =
flutter::testing::OpenFixtureAsMapping("flutter_logo_baked.glb.ipscene");
ASSERT_NE(mapping, nullptr);
std::shared_ptr<scene::Node> gltf_scene = scene::Node::MakeFromFlatbuffer(
*mapping, *GetContext()->GetResourceAllocator());
ASSERT_NE(gltf_scene, nullptr);
auto callback = [&](AiksContext& renderer) -> std::optional<Picture> {
Paint paint;
static Scalar distance = 2;
static Scalar y_pos = 0;
static Scalar fov = 45;
if (AiksTest::ImGuiBegin("Controls", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SliderFloat("Distance", &distance, 0, 4);
ImGui::SliderFloat("Y", &y_pos, -3, 3);
ImGui::SliderFloat("FOV", &fov, 1, 180);
ImGui::End();
}
Scalar angle = GetSecondsElapsed();
auto camera_position =
Vector3(distance * std::sin(angle), y_pos, -distance * std::cos(angle));
paint.color_source = ColorSource::MakeScene(
gltf_scene,
Matrix::MakePerspective(Degrees(fov), GetWindowSize(), 0.1, 1000) *
Matrix::MakeLookAt(camera_position, {0, 0, 0}, {0, 1, 0}));
Canvas canvas;
canvas.DrawPaint(Paint{.color = Color::MakeRGBA8(0xf9, 0xf9, 0xf9, 0xff)});
canvas.Scale(GetContentScale());
canvas.DrawPaint(paint);
return canvas.EndRecordingAsPicture();
};
ASSERT_TRUE(OpenPlaygroundHere(callback));
}
#endif // IMPELLER_ENABLE_3D
TEST_P(AiksTest, PaintWithFilters) {
// validate that a paint with a color filter "HasFilters", no other filters
// impact this setting.
Paint paint;
ASSERT_FALSE(paint.HasColorFilter());
paint.color_filter =
ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Blue());
ASSERT_TRUE(paint.HasColorFilter());
paint.image_filter = ImageFilter::MakeBlur(Sigma(1.0), Sigma(1.0),
FilterContents::BlurStyle::kNormal,
Entity::TileMode::kClamp);
ASSERT_TRUE(paint.HasColorFilter());
paint.mask_blur_descriptor = {};
ASSERT_TRUE(paint.HasColorFilter());
paint.color_filter = nullptr;
ASSERT_FALSE(paint.HasColorFilter());
}
TEST_P(AiksTest, DrawPaintAbsorbsClears) {
Canvas canvas;
canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawPaint({.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
Picture picture = canvas.EndRecordingAsPicture();
auto expected = Color::Red().Blend(Color::CornflowerBlue().WithAlpha(0.75),
BlendMode::kSourceOver);
ASSERT_EQ(picture.pass->GetClearColor(), expected);
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 0llu);
}
// This is important to enforce with texture reuse, since cached textures need
// to be cleared before reuse.
TEST_P(AiksTest,
ParentSaveLayerCreatesRenderPassWhenChildBackdropFilterIsPresent) {
Canvas canvas;
canvas.SaveLayer({}, std::nullopt, ImageFilter::MakeMatrix(Matrix(), {}));
canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawPaint({.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
canvas.Restore();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(),
GetBackend() == PlaygroundBackend::kOpenGLES ? 4llu : 3llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 0llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClears) {
Canvas canvas;
canvas.DrawRect(Rect::MakeXYWH(0, 0, 300, 300),
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawRect(Rect::MakeXYWH(0, 0, 300, 300),
{.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 0llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClearsNegativeRRect) {
Canvas canvas;
canvas.DrawRRect(Rect::MakeXYWH(0, 0, 300, 300), {5.0, 5.0},
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawRRect(Rect::MakeXYWH(0, 0, 300, 300), {5.0, 5.0},
{.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 2llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClearsNegativeRotation) {
Canvas canvas;
canvas.Translate(Vector3(150.0, 150.0, 0.0));
canvas.Rotate(Degrees(45.0));
canvas.Translate(Vector3(-150.0, -150.0, 0.0));
canvas.DrawRect(Rect::MakeXYWH(0, 0, 300, 300),
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 1llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClearsNegative) {
Canvas canvas;
canvas.DrawRect(Rect::MakeXYWH(0, 0, 300, 300),
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawRect(Rect::MakeXYWH(0, 0, 300, 300),
{.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {301, 301});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 2llu);
}
TEST_P(AiksTest, ClipRectElidesNoOpClips) {
Canvas canvas(Rect::MakeXYWH(0, 0, 100, 100));
canvas.ClipRect(Rect::MakeXYWH(0, 0, 100, 100));
canvas.ClipRect(Rect::MakeXYWH(-100, -100, 300, 300));
canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawPaint({.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
Picture picture = canvas.EndRecordingAsPicture();
auto expected = Color::Red().Blend(Color::CornflowerBlue().WithAlpha(0.75),
BlendMode::kSourceOver);
ASSERT_EQ(picture.pass->GetClearColor(), expected);
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context, nullptr);
std::shared_ptr<Texture> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 0llu);
}
TEST_P(AiksTest, ClearColorOptimizationDoesNotApplyForBackdropFilters) {
Canvas canvas;
canvas.SaveLayer({}, std::nullopt,
ImageFilter::MakeBlur(Sigma(3), Sigma(3),
FilterContents::BlurStyle::kNormal,
Entity::TileMode::kClamp));
canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawPaint({.color = Color::CornflowerBlue().WithAlpha(0.75),
.blend_mode = BlendMode::kSourceOver});
canvas.Restore();
Picture picture = canvas.EndRecordingAsPicture();
std::optional<Color> actual_color;
bool found_subpass = false;
picture.pass->IterateAllElements([&](EntityPass::Element& element) -> bool {
if (auto subpass = std::get_if<std::unique_ptr<EntityPass>>(&element)) {
actual_color = subpass->get()->GetClearColor();
found_subpass = true;
}
// Fail if the first element isn't a subpass.
return true;
});
EXPECT_TRUE(found_subpass);
EXPECT_FALSE(actual_color.has_value());
}
TEST_P(AiksTest, OpaqueEntitiesGetCoercedToSource) {
Canvas canvas;
canvas.Scale(Vector2(1.618, 1.618));
canvas.DrawCircle(Point(), 10,
{
.color = Color::CornflowerBlue(),
.blend_mode = BlendMode::kSourceOver,
});
Picture picture = canvas.EndRecordingAsPicture();
// Extract the SolidColorSource.
// Entity entity;
std::vector<Entity> entity;
std::shared_ptr<SolidColorContents> contents;
picture.pass->IterateAllEntities([e = &entity, &contents](Entity& entity) {
if (ScalarNearlyEqual(entity.GetTransform().GetScale().x, 1.618f)) {
contents =
std::static_pointer_cast<SolidColorContents>(entity.GetContents());
e->emplace_back(entity.Clone());
return false;
}
return true;
});
ASSERT_TRUE(entity.size() >= 1);
ASSERT_TRUE(contents->IsOpaque({}));
ASSERT_EQ(entity[0].GetBlendMode(), BlendMode::kSource);
}
TEST_P(AiksTest, SolidColorApplyColorFilter) {
auto contents = SolidColorContents();
contents.SetColor(Color::CornflowerBlue().WithAlpha(0.75));
auto result = contents.ApplyColorFilter([](const Color& color) {
return color.Blend(Color::LimeGreen().WithAlpha(0.75), BlendMode::kScreen);
});
ASSERT_TRUE(result);
ASSERT_COLOR_NEAR(contents.GetColor(),
Color(0.424452, 0.828743, 0.79105, 0.9375));
}
TEST_P(AiksTest, CorrectClipDepthAssignedToEntities) {
Canvas canvas; // Depth 1 (base pass)
canvas.DrawRRect(Rect::MakeLTRB(0, 0, 100, 100), {10, 10}, {}); // Depth 2
canvas.Save();
{
canvas.ClipRRect(Rect::MakeLTRB(0, 0, 50, 50), {10, 10}, {}); // Depth 4
canvas.SaveLayer({}); // Depth 4
{
canvas.DrawRRect(Rect::MakeLTRB(0, 0, 50, 50), {10, 10}, {}); // Depth 3
}
canvas.Restore(); // Restore the savelayer.
}
canvas.Restore(); // Depth 5 -- this will no longer append a restore entity
// once we switch to the clip depth approach.
auto picture = canvas.EndRecordingAsPicture();
std::vector<uint32_t> expected = {
2, // DrawRRect
4, // ClipRRect -- Has a depth value equal to the max depth of all the
// content it affect. In this case, the SaveLayer and all
// its contents are affected.
4, // SaveLayer -- The SaveLayer is drawn to the parent pass after its
// contents are rendered, so it should have a depth value
// greater than all its contents.
3, // DrawRRect
5, // Restore (no longer necessary when clipping on the depth buffer)
};
std::vector<uint32_t> actual;
picture.pass->IterateAllElements([&](EntityPass::Element& element) -> bool {
if (auto* subpass = std::get_if<std::unique_ptr<EntityPass>>(&element)) {
actual.push_back(subpass->get()->GetClipDepth());
}
if (Entity* entity = std::get_if<Entity>(&element)) {
actual.push_back(entity->GetClipDepth());
}
return true;
});
ASSERT_EQ(actual.size(), expected.size());
for (size_t i = 0; i < expected.size(); i++) {
EXPECT_EQ(expected[i], actual[i]) << "Index: " << i;
}
}
} // namespace testing
} // namespace impeller
// █████████████████████████████████████████████████████████████████████████████
// █ NOTICE: Before adding new tests to this file consider adding it to one of
// █ the subdivisions of AiksTest to avoid having one massive file.
// █
// █ Subdivisions:
// █ - aiks_blend_unittests.cc
// █ - aiks_gradient_unittests.cc
// █████████████████████████████████████████████████████████████████████████████