| // 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 |
| // █████████████████████████████████████████████████████████████████████████████ |