| // 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::DictValue& 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::DictValue& 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::DictValue& 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 |