| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/gl/os_compositor_tree_base.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/functional/function_ref.h" |
| #include "base/notreached.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/trace_event/traced_value.h" |
| #include "base/values.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/gfx/overlay_layer_id.h" |
| |
| namespace gl { |
| namespace { |
| |
| // Concise alias to create a test layer ID. |
| gfx::OverlayLayerId LayerId(uint32_t layer_id) { |
| return gfx::OverlayLayerId::MakeForTesting(layer_id); |
| } |
| |
| // A mock OS compositor tree, imitating Direct Composition or Core Animation. It |
| // minimally implements a DAG with a root node and basic operations to |
| // manipulate it. It uses `gfx::OverlayLayerId` as the node type. |
| class MiniTree { |
| public: |
| using node_type = gfx::OverlayLayerId; |
| |
| // This contains information about accesses to the `MiniTree`. It's intended |
| // to aid debugging test failures. |
| struct Change { |
| enum class Type { |
| kCreate, |
| kRemoveChild, |
| kAddChild, |
| kDestroy, |
| kCommit, |
| }; |
| |
| Type type; |
| node_type node; |
| // For `kRemoveChild`, `kAddChild` |
| node_type child; |
| // For `kAddChild` |
| std::optional<node_type> below; |
| |
| base::Value ToValue() const { |
| base::Value value(base::Value::Type::DICT); |
| base::Value::Dict& dict = value.GetDict(); |
| switch (type) { |
| case Type::kCreate: |
| dict.Set("_type", "Create"); |
| dict.Set("node", node.ToString()); |
| break; |
| case Type::kRemoveChild: |
| dict.Set("_type", "RemoveChild"); |
| dict.Set("parent", node.ToString()); |
| dict.Set("child", child.ToString()); |
| break; |
| case Type::kAddChild: |
| dict.Set("_type", "AddChild"); |
| dict.Set("parent", node.ToString()); |
| dict.Set("child", child.ToString()); |
| if (below) { |
| dict.Set("below", below->ToString()); |
| } else { |
| // Nothing is below the node, so it is added below all siblings. |
| } |
| break; |
| case Type::kDestroy: |
| dict.Set("_type", "Destroy"); |
| dict.Set("node", node.ToString()); |
| break; |
| case Type::kCommit: |
| dict.Set("_type", "Commit"); |
| break; |
| } |
| return value; |
| } |
| }; |
| |
| MiniTree() |
| : root_node_( |
| // Insert the implicit root node. |
| CreateNode(gfx::OverlayLayerId())) { |
| // We're not interested in root node changes since they are the same for all |
| // trees. |
| changes_.clear(); |
| nodes_touched_this_frame_.clear(); |
| } |
| |
| node_type CreateNode(const node_type& node) { |
| CHECK(!hierarchy_.contains(node) || hierarchy_.at(node).empty()); |
| |
| hierarchy_.insert({node, {}}); |
| |
| changes_.push_back(Change{.type = Change::Type::kCreate, .node = node}); |
| nodes_touched_this_frame_.insert(node); |
| |
| return node; |
| } |
| |
| void RemoveChild(const node_type& parent, const node_type& child) { |
| CHECK(hierarchy_.contains(parent)) << "parent node must exist"; |
| CHECK(hierarchy_.contains(child)) << "child node must exist"; |
| |
| auto& siblings = hierarchy_[parent]; |
| auto it = std::ranges::find(siblings, child); |
| CHECK(it != siblings.end()) |
| << "child node must have been previously added under parent"; |
| siblings.erase(it); |
| |
| changes_.push_back(Change{ |
| .type = Change::Type::kRemoveChild, .node = parent, .child = child}); |
| nodes_touched_this_frame_.insert(child); |
| } |
| |
| void AddChild(const node_type& parent, |
| const node_type& child, |
| const std::optional<node_type>& below) { |
| CHECK(hierarchy_.contains(parent)) << "parent node must exist"; |
| CHECK(hierarchy_.contains(child)) << "child node must exist"; |
| CHECK(std::ranges::none_of(hierarchy_, [&](const auto& entry) { |
| const auto& siblings = entry.second; |
| return std::ranges::find(siblings, child) != siblings.end(); |
| })) << "child node must not be attached to any node"; |
| |
| auto& siblings = hierarchy_[parent]; |
| if (below) { |
| auto insert_after = std::ranges::find(siblings, below.value()); |
| CHECK(insert_after != siblings.end()) << "below node must exist"; |
| insert_after++; |
| siblings.insert(insert_after, child); |
| } else { |
| siblings.insert(siblings.begin(), child); |
| } |
| |
| changes_.push_back(Change{.type = Change::Type::kAddChild, |
| .node = parent, |
| .child = child, |
| .below = below}); |
| nodes_touched_this_frame_.insert(child); |
| } |
| |
| struct CommitStats { |
| std::vector<Change> changes; |
| base::flat_set<gfx::OverlayLayerId> nodes_touched_this_frame; |
| }; |
| |
| CommitStats Commit() { |
| changes_.push_back(Change{.type = Change::Type::kCommit}); |
| |
| // Trim the tree storage |
| { |
| base::flat_set<node_type> referenced_nodes{root_node()}; |
| WalkChildrenRecursive( |
| root_node(), [&](const node_type& parent, const node_type& node) { |
| referenced_nodes.insert(node); |
| }); |
| |
| auto it = hierarchy_.begin(); |
| while (it != hierarchy_.end()) { |
| if (!referenced_nodes.contains(it->first)) { |
| changes_.push_back( |
| Change{.type = Change::Type::kDestroy, .node = it->first}); |
| it = hierarchy_.erase(it); |
| } else { |
| it++; |
| } |
| } |
| } |
| |
| return { |
| .changes = std::move(changes_), |
| .nodes_touched_this_frame = std::move(nodes_touched_this_frame_), |
| }; |
| } |
| |
| node_type root_node() const { return root_node_; } |
| |
| base::Value RootNodeToValue() const { |
| return NodeToValueRecursive(root_node()); |
| } |
| |
| private: |
| using RecursiveWalkCallback = |
| base::FunctionRef<void(const node_type& parent, const node_type& node)>; |
| void WalkChildrenRecursive(const node_type& node, |
| RecursiveWalkCallback callback) const { |
| for (const auto& child : hierarchy_.at(node)) { |
| callback(node, child); |
| WalkChildrenRecursive(child, callback); |
| } |
| } |
| |
| base::Value NodeToValueRecursive(node_type node) const { |
| base::Value value(base::Value::Type::DICT); |
| base::Value::Dict& dict = value.GetDict(); |
| dict.Set("_id", node == root_node() ? "root" : node.ToString()); |
| if (!hierarchy_.at(node).empty()) { |
| base::Value children(base::Value::Type::LIST); |
| for (const auto& child : hierarchy_.at(node)) { |
| children.GetList().Append(NodeToValueRecursive(child)); |
| } |
| dict.Set("children", std::move(children)); |
| } |
| return value; |
| } |
| |
| std::vector<Change> changes_; |
| base::flat_set<node_type> nodes_touched_this_frame_; |
| |
| // Represents a N-ary tree where each node has ordered (back-to-front) |
| // children. We expect this to be acyclic. |
| base::flat_map<node_type, std::vector<node_type>> hierarchy_; |
| |
| const node_type root_node_; |
| }; |
| |
| TEST(MiniTreeTest, EmptyTreesAreEqual) { |
| MiniTree tree1; |
| MiniTree tree2; |
| EXPECT_EQ(tree1.RootNodeToValue(), tree2.RootNodeToValue()); |
| } |
| |
| TEST(MiniTreeTest, SimpleTreesAreEqual) { |
| MiniTree tree1; |
| { |
| tree1.AddChild(tree1.root_node(), tree1.CreateNode(LayerId(1)), {}); |
| tree1.AddChild(tree1.root_node(), tree1.CreateNode(LayerId(2)), {}); |
| } |
| |
| MiniTree tree2; |
| { |
| tree2.AddChild(tree2.root_node(), tree2.CreateNode(LayerId(1)), {}); |
| tree2.AddChild(tree2.root_node(), tree2.CreateNode(LayerId(2)), {}); |
| } |
| |
| EXPECT_EQ(tree1.RootNodeToValue(), tree2.RootNodeToValue()); |
| } |
| |
| TEST(MiniTreeTest, SimpleTreesWithDifferentStructureAreNotEqual) { |
| MiniTree tree1; |
| { |
| tree1.AddChild(tree1.root_node(), tree1.CreateNode(LayerId(1)), {}); |
| tree1.AddChild(tree1.root_node(), tree1.CreateNode(LayerId(2)), {}); |
| } |
| |
| MiniTree tree2; |
| { |
| tree2.AddChild(tree2.root_node(), tree2.CreateNode(LayerId(1)), {}); |
| } |
| |
| EXPECT_NE(tree1.RootNodeToValue(), tree2.RootNodeToValue()); |
| } |
| |
| TEST(MiniTreeTest, SimpleTreesWithSameStructureButDifferentContentAreNotEqual) { |
| MiniTree tree1; |
| { |
| tree1.AddChild(tree1.root_node(), tree1.CreateNode(LayerId(1)), {}); |
| tree1.AddChild(tree1.root_node(), tree1.CreateNode(LayerId(2)), {}); |
| } |
| |
| MiniTree tree2; |
| { |
| tree2.AddChild(tree2.root_node(), tree2.CreateNode(LayerId(1)), {}); |
| tree2.AddChild(tree2.root_node(), tree2.CreateNode(LayerId(3)), {}); |
| } |
| |
| EXPECT_NE(tree1.RootNodeToValue(), tree2.RootNodeToValue()); |
| } |
| |
| // A minimal overlay layer type. This is the minimum required by |
| // `OsCompositorTreeBase`. |
| struct TestOverlayParams { |
| TestOverlayParams() = default; |
| |
| TestOverlayParams(TestOverlayParams&&) = default; |
| TestOverlayParams& operator=(TestOverlayParams&&) = default; |
| |
| int z_order; |
| gfx::OverlayLayerId layer_id; |
| gfx::OverlayLayerId parent_layer_id; |
| |
| base::Value ToValue() const { |
| base::Value value(base::Value::Type::DICT); |
| base::Value::Dict& dict = value.GetDict(); |
| dict.Set("z_order", z_order); |
| dict.Set("layer_id", layer_id.ToString()); |
| dict.Set("parent_layer_id", parent_layer_id.ToString()); |
| return value; |
| } |
| }; |
| |
| // Helper functions to build test input (i.e. overlay params list) and expected |
| // test output (i.e. `MiniTree` generated "from scratch"). This allows test to |
| // be written with mostly declarative input. |
| class TestTreeBuilder { |
| public: |
| struct Layer { |
| gfx::OverlayLayerId id; |
| // Ordered back-to-front. |
| std::vector<Layer> children; |
| |
| Layer() = default; |
| Layer(uint32_t layer_id, std::vector<Layer> child_layers) |
| : id(gfx::OverlayLayerId::MakeForTesting(layer_id)), |
| children(child_layers) {} |
| }; |
| |
| static MiniTree MiniTreeFromRootLayers(std::vector<Layer> root_layers) { |
| MiniTree tree; |
| WalkRootTopologically(root_layers, |
| [&](const gfx::OverlayLayerId& parent, |
| const std::optional<gfx::OverlayLayerId>& below, |
| const gfx::OverlayLayerId& id, int z_order) { |
| tree.AddChild(parent, tree.CreateNode(id), below); |
| }); |
| return tree; |
| } |
| |
| // Returns a topologically sorted list of overlays adding the correct z-index |
| // relative to siblings. |
| static std::vector<TestOverlayParams> OverlaysFromRootLayers( |
| std::vector<Layer> root_layers) { |
| std::vector<TestOverlayParams> overlays; |
| WalkRootTopologically(root_layers, |
| [&](const gfx::OverlayLayerId& parent, |
| const std::optional<gfx::OverlayLayerId>& below, |
| const gfx::OverlayLayerId& id, int z_order) { |
| overlays.emplace_back(); |
| overlays.back().layer_id = id; |
| overlays.back().parent_layer_id = parent; |
| overlays.back().z_order = z_order; |
| }); |
| return overlays; |
| } |
| |
| private: |
| using TopologicalWalkCallback = |
| base::FunctionRef<void(const gfx::OverlayLayerId& parent, |
| const std::optional<gfx::OverlayLayerId>& below, |
| const gfx::OverlayLayerId& id, |
| int z_order)>; |
| |
| static void WalkRootTopologically(std::vector<Layer>& root_layers, |
| TopologicalWalkCallback callback) { |
| Layer root_layer; |
| root_layer.id = gfx::OverlayLayerId(); |
| root_layer.children = root_layers; |
| WalkTopologically(root_layer, std::move(callback)); |
| } |
| |
| static void WalkTopologically(const Layer& layer, |
| TopologicalWalkCallback callback) { |
| int z_order = 1; |
| std::optional<gfx::OverlayLayerId> below; |
| for (const Layer& child_layer : layer.children) { |
| callback(layer.id, below, child_layer.id, z_order++); |
| WalkTopologically(child_layer, callback); |
| below = child_layer.id; |
| } |
| } |
| }; |
| |
| // Alias to make tests less verbose. |
| using Layer = TestTreeBuilder::Layer; |
| |
| // A minimal layer type. This correctly returns `true` from `Update` if things |
| // have changed. |
| class MiniTreeNodeWrapper { |
| public: |
| MiniTreeNodeWrapper() = default; |
| |
| MiniTreeNodeWrapper(const MiniTreeNodeWrapper&) = delete; |
| MiniTreeNodeWrapper& operator=(const MiniTreeNodeWrapper&) = delete; |
| |
| void unsafe_set_below_layer(const MiniTreeNodeWrapper* below_layer) { |
| below_node_ = below_layer ? below_layer->container_node() : std::nullopt; |
| } |
| |
| bool Update(MiniTree& tree, |
| const gfx::OverlayLayerId& layer_id, |
| MiniTree::node_type parent_node, |
| std::optional<MiniTree::node_type> below_node) { |
| bool did_change = false; |
| |
| std::optional<MiniTree::node_type> old_parent_node = parent_node_; |
| |
| auto SetField = [&did_change](auto& field, auto& parameter) -> bool { |
| const bool changed = field != parameter; |
| if (changed) { |
| field = std::move(parameter); |
| did_change = true; |
| } |
| return changed; |
| }; |
| |
| const bool parent_node_changed = SetField(parent_node_, parent_node); |
| const bool below_node_changed = SetField(below_node_, below_node); |
| |
| if (!container_node()) { |
| container_node_ = tree.CreateNode(layer_id); |
| } |
| |
| if (parent_node_changed || below_node_changed) { |
| if (old_parent_node) { |
| tree.RemoveChild(old_parent_node.value(), container_node().value()); |
| } |
| |
| tree.AddChild(parent_node_.value(), container_node().value(), |
| below_node_); |
| } |
| |
| return did_change; |
| } |
| |
| std::optional<MiniTree::node_type> container_node() const { |
| return container_node_; |
| } |
| std::optional<MiniTree::node_type> parent_node() const { |
| return parent_node_; |
| } |
| |
| private: |
| std::optional<MiniTree::node_type> container_node_; |
| std::optional<MiniTree::node_type> parent_node_; |
| std::optional<MiniTree::node_type> below_node_; |
| }; |
| |
| class TestCompositorTree |
| : public OsCompositorTreeBase<TestOverlayParams, MiniTreeNodeWrapper> { |
| public: |
| explicit TestCompositorTree(TestCompositorTree::UpdateMode update_mode) |
| : OsCompositorTreeBase(update_mode) {} |
| |
| ~TestCompositorTree() override = default; |
| |
| const MiniTree& mini_tree() const { return mini_tree_; } |
| |
| const MiniTree::CommitStats& last_frame_commit_stats() const { |
| return last_frame_commit_stats_; |
| } |
| |
| protected: |
| bool UpdateLayer(const TestOverlayParams& overlay, |
| const MiniTreeNodeWrapper* parent_layer, |
| const MiniTreeNodeWrapper* below_layer, |
| MiniTreeNodeWrapper& layer) override { |
| // This test layer is logically a single node (not a stack of several nodes |
| // that apply transform, clipping, etc). We can consider the top-level |
| // container node to be the same node that contains the children. |
| const MiniTree::node_type parent_node = |
| parent_layer ? parent_layer->container_node().value() |
| : mini_tree_.root_node(); |
| |
| return layer.Update( |
| mini_tree_, overlay.layer_id, parent_node, |
| below_layer ? below_layer->container_node() : std::nullopt); |
| } |
| |
| void DestroyLayer(std::unique_ptr<MiniTreeNodeWrapper> layer) override { |
| mini_tree_.RemoveChild(layer->parent_node().value(), |
| layer->container_node().value()); |
| } |
| |
| bool CommitTree() override { |
| last_frame_commit_stats_ = mini_tree_.Commit(); |
| return true; |
| } |
| |
| private: |
| MiniTree mini_tree_; |
| MiniTree::CommitStats last_frame_commit_stats_; |
| }; |
| |
| class OsCompositorTreeBaseTest |
| : public testing::TestWithParam<TestCompositorTree::UpdateMode> { |
| public: |
| static std::string GetParamName( |
| const testing::TestParamInfo<ParamType>& info) { |
| switch (info.param) { |
| case TestCompositorTree::UpdateMode::kFromScratch: |
| return "FromScratch"; |
| case TestCompositorTree::UpdateMode:: |
| kIncrementalNoPatchSiblingsOptimization: |
| return "IncrementalNoPatchSiblingsOptimization"; |
| case TestCompositorTree::UpdateMode::kIncremental: |
| return "Incremental"; |
| } |
| NOTREACHED(); |
| } |
| |
| OsCompositorTreeBaseTest() : tree_(GetParam()) {} |
| |
| // This function creates an overlay list from `layers`, passes it to `tree_` |
| // and compares `tree_`'s incremental output with `layers`. |
| // |
| // - `layers` defines a tree where they are the children of an implicit root |
| // layer. |
| // - `expected_num_layers_modified` is the expected number of layers |
| // touched in the fully incremental mode. This function will adjust the |
| // expectation for other modes. |
| void UpdateTree(std::vector<Layer> layers, int expected_num_layers_modified) { |
| const base::Value prev_tree = tree_.mini_tree().RootNodeToValue(); |
| |
| std::vector<TestOverlayParams> overlays = |
| TestTreeBuilder::OverlaysFromRootLayers(layers); |
| |
| // Dump the generated overlay candidates to help understand the input tree. |
| base::Value overlays_list(base::Value::Type::LIST); |
| for (const auto& overlay : overlays) { |
| overlays_list.GetList().Append(overlay.ToValue()); |
| } |
| SCOPED_TRACE(base::StringPrintf("Generated overlay candidates = %s", |
| overlays_list.DebugString())); |
| |
| EXPECT_TRUE(tree_.UpdateTree(overlays)); |
| |
| const base::Value actual_tree = tree_.mini_tree().RootNodeToValue(); |
| const base::Value expected_tree = |
| TestTreeBuilder::MiniTreeFromRootLayers(layers).RootNodeToValue(); |
| |
| // Dump the previous tree and the set of changes that happened to help |
| // understand why. |
| const auto& commit_stats = tree_.last_frame_commit_stats(); |
| base::Value changes_list(base::Value::Type::LIST); |
| for (const auto& change : commit_stats.changes) { |
| changes_list.GetList().Append(change.ToValue()); |
| } |
| base::Value touched_layer_ids_list(base::Value::Type::LIST); |
| for (const auto& touched_layer_id : commit_stats.nodes_touched_this_frame) { |
| touched_layer_ids_list.GetList().Append(touched_layer_id.ToString()); |
| } |
| SCOPED_TRACE(base::StringPrintf( |
| "Incremental update info:\n" |
| "Prev tree: %s\n" |
| "Changes from this update: %s\n" |
| "Layers directly touched: %s\n" |
| "Current tree: %s\n" |
| "Run with `--vmodule=os_compositor_tree_base=3` for more output.", |
| prev_tree.DebugString(), changes_list.DebugString(), |
| touched_layer_ids_list.DebugString(), actual_tree.DebugString())); |
| |
| // The output tree should always be the same, regardless of how we update. |
| EXPECT_EQ(actual_tree, expected_tree); |
| |
| switch (GetParam()) { |
| case TestCompositorTree::UpdateMode::kFromScratch: |
| EXPECT_EQ(static_cast<size_t>( |
| tree_.num_layers_modified_last_frame_for_testing()), |
| overlays.size()); |
| break; |
| case TestCompositorTree::UpdateMode:: |
| kIncrementalNoPatchSiblingsOptimization: |
| // We only really care about the fully optimized incremental case, but |
| // if the patch optimization is causing us to touch more trees, we |
| // should know about it. |
| EXPECT_GE(tree_.num_layers_modified_last_frame_for_testing(), |
| expected_num_layers_modified); |
| break; |
| case TestCompositorTree::UpdateMode::kIncremental: |
| EXPECT_EQ(tree_.num_layers_modified_last_frame_for_testing(), |
| expected_num_layers_modified); |
| break; |
| } |
| } |
| |
| // Same as `UpdateTree(layers, size(layers))`. Expected to be called from the |
| // default empty initialized state. Intended for initializing a test state, |
| // but can be skipped if the empty state is desired. |
| void InitializeTree(std::vector<Layer> layers) { |
| UpdateTree(layers, TestTreeBuilder::OverlaysFromRootLayers(layers).size()); |
| } |
| |
| TestCompositorTree tree_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| OsCompositorTreeBaseTest, |
| testing::ValuesIn({ |
| TestCompositorTree::UpdateMode::kFromScratch, |
| TestCompositorTree::UpdateMode::kIncrementalNoPatchSiblingsOptimization, |
| TestCompositorTree::UpdateMode::kIncremental, |
| }), |
| &OsCompositorTreeBaseTest::GetParamName); |
| |
| TEST_P(OsCompositorTreeBaseTest, CommitEmpty) { |
| UpdateTree({}, 0); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, InsertOneIntoEmpty) { |
| UpdateTree( |
| { |
| Layer(1, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, LayerNotReused) { |
| InitializeTree({ |
| Layer(1, {}), |
| }); |
| |
| // We expect layer 1 to be removed (and destroyed) and a new layer 2 added. |
| UpdateTree( |
| { |
| Layer(2, {}), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, DeleteOne) { |
| InitializeTree({ |
| Layer(1, {}), |
| }); |
| |
| UpdateTree({}, 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, DeleteOneAndReAddIt) { |
| InitializeTree({ |
| Layer(1, {}), |
| }); |
| |
| UpdateTree({}, 1); |
| |
| UpdateTree( |
| { |
| Layer(1, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, DeleteTwo) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| }); |
| UpdateTree({}, 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, DeleteLayer) { |
| InitializeTree({ |
| Layer(1, |
| { |
| Layer(2, {}), |
| }), |
| }); |
| UpdateTree({}, 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, InsertTwoIntoEmpty) { |
| UpdateTree( |
| { |
| Layer(1, {}), |
| Layer(2, {}), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, InsertLayerIntoEmpty) { |
| UpdateTree( |
| { |
| Layer(1, |
| { |
| Layer(2, {}), |
| }), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, SwitchOrderOfTwo) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| }); |
| UpdateTree( |
| { |
| Layer(2, {}), |
| Layer(1, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ThreeSwapFirstTwo) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| // Swap index 0 and 1 |
| UpdateTree( |
| { |
| Layer(2, {}), |
| Layer(1, {}), |
| Layer(3, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ThreeSwapLastTwo) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| // Swap index 1 and 2 |
| UpdateTree( |
| { |
| Layer(1, {}), |
| Layer(3, {}), |
| Layer(2, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ThreeSwapOuter) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| // Swap index 0 and 2 |
| // This requires touching two layers. |
| UpdateTree( |
| { |
| Layer(3, {}), |
| Layer(2, {}), |
| Layer(1, {}), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ThreeRotateToBelow) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(3, {}), |
| Layer(1, {}), |
| Layer(2, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ThreeRotateToAbove) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| // This requires touching two layers, due to the fact we walk the tree in |
| // overlay candidate order. |
| UpdateTree( |
| { |
| Layer(2, {}), |
| Layer(3, {}), |
| Layer(1, {}), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, MoveChildToSiblingOfParent) { |
| InitializeTree({ |
| Layer(1, |
| { |
| Layer(3, {}), |
| }), |
| Layer(2, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, MoveMultipleSiblingsToNewParent) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(1, |
| { |
| Layer(2, {}), |
| Layer(3, {}), |
| }), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ReparentSiblingsToChain) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(1, |
| { |
| Layer(2, |
| { |
| Layer(3, {}), |
| }), |
| }), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, ReparentLayer) { |
| InitializeTree({ |
| Layer(1, |
| { |
| Layer(3, |
| { |
| Layer(2, {}), |
| }), |
| }), |
| }); |
| |
| // We move layer 3 while layer 2 is a child of it. Layer 2 does not need to be |
| // updated in this case. |
| UpdateTree( |
| { |
| Layer(3, |
| { |
| Layer(2, {}), |
| }), |
| Layer(1, {}), |
| }, |
| 1); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, RemoveAndAddAtStart) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(10, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, RemoveAndAddAtMiddle) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(1, {}), |
| Layer(10, {}), |
| Layer(3, {}), |
| }, |
| 2); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTest, RemoveAndAddAtEnd) { |
| InitializeTree({ |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(3, {}), |
| }); |
| |
| UpdateTree( |
| { |
| Layer(1, {}), |
| Layer(2, {}), |
| Layer(10, {}), |
| }, |
| 2); |
| } |
| |
| #if EXPENSIVE_DCHECKS_ARE_ON() && defined(GTEST_HAS_DEATH_TEST) |
| // Check that `OsCompositorTreeBase` rejects invalid input since invalid input |
| // can invalidate its assumptions. |
| class OsCompositorTreeBaseTestInvalidInput : public OsCompositorTreeBaseTest {}; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| OsCompositorTreeBaseTestInvalidInput, |
| testing::Values(TestCompositorTree::UpdateMode::kIncremental), |
| &OsCompositorTreeBaseTest::GetParamName); |
| |
| constexpr std::string_view kZOrderCheckMessage = "Siblings must be sorted."; |
| constexpr std::string_view kMustHaveLayerIdMessage = |
| "Overlay must have non-default layer ID."; |
| constexpr std::string_view kHierarchyCheckMessage = |
| "Overlays must be topologically sorted."; |
| constexpr std::string_view kLayerIdUniquenessCheckMessage = |
| "Overlay layer IDs must all be unique."; |
| |
| TEST_P(OsCompositorTreeBaseTestInvalidInput, UnsortedSiblings) { |
| std::vector<TestOverlayParams> overlays; |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 2; |
| overlays.back().layer_id = LayerId(2); |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(1); |
| |
| EXPECT_DEATH(tree_.UpdateTree(overlays), kZOrderCheckMessage); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTestInvalidInput, UnsortedSiblingsInLayer) { |
| std::vector<TestOverlayParams> overlays; |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(1); |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 2; |
| overlays.back().layer_id = LayerId(2); |
| overlays.back().parent_layer_id = LayerId(1); |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(3); |
| overlays.back().parent_layer_id = LayerId(1); |
| |
| EXPECT_DEATH(tree_.UpdateTree(overlays), kZOrderCheckMessage); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTestInvalidInput, NoSameZOrder) { |
| std::vector<TestOverlayParams> overlays; |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(1); |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(2); |
| |
| EXPECT_DEATH(tree_.UpdateTree(overlays), kZOrderCheckMessage); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTestInvalidInput, NotTopologicallySorted) { |
| std::vector<TestOverlayParams> overlays; |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(2); |
| overlays.back().parent_layer_id = LayerId(1), |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(1); |
| |
| EXPECT_DEATH(tree_.UpdateTree(overlays), kHierarchyCheckMessage); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTestInvalidInput, NeedsExplicitLayerId) { |
| std::vector<TestOverlayParams> overlays; |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| |
| EXPECT_DEATH(tree_.UpdateTree(overlays), kMustHaveLayerIdMessage); |
| } |
| |
| TEST_P(OsCompositorTreeBaseTestInvalidInput, NoDuplicateLayerId) { |
| std::vector<TestOverlayParams> overlays; |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 1; |
| overlays.back().layer_id = LayerId(1); |
| |
| overlays.emplace_back(); |
| overlays.back().z_order = 2; |
| overlays.back().layer_id = LayerId(1); |
| |
| EXPECT_DEATH(tree_.UpdateTree(overlays), kLayerIdUniquenessCheckMessage); |
| } |
| #endif |
| |
| class MockTestCompositorTree : public TestCompositorTree { |
| public: |
| MockTestCompositorTree() |
| : TestCompositorTree(TestCompositorTree::UpdateMode::kIncremental) {} |
| |
| MOCK_METHOD(bool, CommitTree, (), (override)); |
| }; |
| |
| TEST(OsCompositorTreeBaseTestCommit, NoUpdatesSkipCommit) { |
| MockTestCompositorTree tree; |
| EXPECT_CALL(tree, CommitTree()).Times(0); |
| EXPECT_TRUE(tree.UpdateTree({})); |
| } |
| |
| TEST(OsCompositorTreeBaseTestCommit, Commit) { |
| MockTestCompositorTree tree; |
| EXPECT_CALL(tree, CommitTree()).WillOnce(testing::Return(true)); |
| const auto overlays = TestTreeBuilder::OverlaysFromRootLayers({ |
| Layer(1, {}), |
| }); |
| EXPECT_TRUE(tree.UpdateTree(overlays)); |
| } |
| |
| TEST(OsCompositorTreeBaseTestCommit, NoUpdatesOnSubsequentFrameSkipCommit) { |
| MockTestCompositorTree tree; |
| { |
| EXPECT_CALL(tree, CommitTree()).WillOnce(testing::Return(true)); |
| const auto overlays = TestTreeBuilder::OverlaysFromRootLayers({ |
| Layer(1, {}), |
| }); |
| EXPECT_TRUE(tree.UpdateTree(overlays)); |
| } |
| |
| { |
| EXPECT_CALL(tree, CommitTree()).Times(0); |
| const auto overlays = TestTreeBuilder::OverlaysFromRootLayers({ |
| Layer(1, {}), |
| }); |
| EXPECT_TRUE(tree.UpdateTree(overlays)); |
| } |
| } |
| |
| TEST(OsCompositorTreeBaseTestCommit, FailedCommit) { |
| MockTestCompositorTree tree; |
| EXPECT_CALL(tree, CommitTree()).WillOnce(testing::Return(false)); |
| const auto overlays = TestTreeBuilder::OverlaysFromRootLayers({ |
| Layer(1, {}), |
| }); |
| EXPECT_FALSE(tree.UpdateTree(overlays)); |
| } |
| |
| } // namespace |
| } // namespace gl |