blob: 888662a7c98ce1831af6d9ca725cc3a78af187e2 [file] [log] [blame]
// Copyright 2020 The Fuchsia 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 "src/ui/scenic/lib/flatland/global_topology_data.h"
#include <lib/syslog/cpp/macros.h>
#include "src/ui/scenic/lib/utils/helpers.h"
#include "src/ui/scenic/lib/utils/logging.h"
namespace {
struct pair_hash {
size_t operator()(const std::pair<flatland::TransformHandle, uint64_t>& p) const noexcept {
return std::hash<flatland::TransformHandle>{}(p.first) ^ std::hash<uint64_t>{}(p.second);
}
};
std::optional<zx_koid_t> GetViewRefKoid(
const flatland::TransformHandle& handle,
const std::unordered_map<flatland::TransformHandle,
std::shared_ptr<const fuchsia::ui::views::ViewRef>>& view_ref_map) {
const auto kv = view_ref_map.find(handle);
if (kv == view_ref_map.end()) {
return std::nullopt;
}
return utils::ExtractKoid(*kv->second);
}
std::optional<size_t> GetViewRefIndex(
zx_koid_t view_ref_koid, const std::vector<flatland::TransformHandle>& transforms,
const std::unordered_map<flatland::TransformHandle,
std::shared_ptr<const fuchsia::ui::views::ViewRef>>& view_refs) {
for (const auto& [transform, view_ref] : view_refs) {
if (view_ref_koid == utils::ExtractKoid(*view_ref)) {
// Found |view_ref_koid|, now calculate the index of its root transform in |transforms|.
for (size_t start = 0; start < transforms.size(); ++start) {
if (transforms[start] == transform) {
return start;
}
}
FX_DCHECK(false) << "view ref root transform not in transforms vector";
}
}
// |view_ref_koid| was not found.
return std::nullopt;
}
// Returns the last index (exclusive) of the subtree rooted at |start|.
//
// Prerequisite: |start| was returned from `GetViewRefIndex()`
size_t GetSubtreeEndIndex(size_t start, const std::vector<flatland::TransformHandle>& transforms,
const std::vector<size_t>& parent_indices) {
FX_DCHECK(start < transforms.size()) << "precondition";
// We need to be careful about the case where start == 0, since in that case hitting the global
// root and hitting start are identical. It is simpler to handle this case explicitly, and then in
// the loop below we have this additional guarantee.
if (start == 0) {
return transforms.size();
}
// |end| is an exclusive index.
size_t end = start + 1;
// This is an O(n logn) op. We can make it O(n) if performance needs dictate.
while (end < transforms.size()) {
// Do an ancestor check to see if the current transform is a descendant of |start_node|.
size_t cur_idx = end;
while (cur_idx != start && cur_idx != 0) {
cur_idx = parent_indices[cur_idx];
}
// We have ran up the ancestor tree until we hit the root of the entire tree - exit.
if (cur_idx == 0) {
break;
}
++end;
}
return end;
}
} // namespace
namespace flatland {
// static
GlobalTopologyData GlobalTopologyData::ComputeGlobalTopologyData(
const UberStruct::InstanceMap& uber_structs, const LinkTopologyMap& links,
TransformHandle::InstanceId link_instance_id, TransformHandle root) {
// There should never be an UberStruct for the |link_instance_id|.
FX_DCHECK(uber_structs.find(link_instance_id) == uber_structs.end());
#ifdef USE_FLATLAND_VERBOSE_LOGGING
{
std::ostringstream str;
str << "ComputeGlobalTopologyData(): Dumping UberStructs:\n";
for (auto& kv : uber_structs) {
str << *kv.second << "...................\n";
}
FLATLAND_VERBOSE_LOG << str.str();
}
#endif
// This is a stack of vector "iterators". We store the raw index, instead of an iterator, so that
// we can do index comparisons.
std::vector<std::pair<const TransformGraph::TopologyVector&, /*local_index=*/size_t>>
vector_stack;
// This is a stack of global parent indices and the number of children left to process for that
// parent.
struct ParentChildIterator {
size_t parent_index = 0;
size_t children_left = 0;
};
std::vector<ParentChildIterator> parent_counts;
TopologyVector topology_vector;
ChildCountVector child_counts;
ParentIndexVector parent_indices;
std::unordered_set<TransformHandle> live_transforms;
std::unordered_map<TransformHandle, std::shared_ptr<const fuchsia::ui::views::ViewRef>> view_refs;
std::unordered_map<TransformHandle, TransformHandle> root_transforms;
std::unordered_map<TransformHandle, std::string> debug_names;
std::unordered_map<TransformHandle, TransformClipRegion> clip_regions;
// If we don't have the root in the map, the topology will be empty.
const auto root_uber_struct_kv = uber_structs.find(root.GetInstanceId());
if (root_uber_struct_kv != uber_structs.cend()) {
vector_stack.emplace_back(root_uber_struct_kv->second->local_topology, 0);
}
while (!vector_stack.empty()) {
auto& [vector, iterator_index] = vector_stack.back();
// If we are finished with a vector, pop back to the previous vector.
if (iterator_index >= vector.size()) {
FX_DCHECK(iterator_index == vector.size());
vector_stack.pop_back();
continue;
}
const auto& current_entry = vector[iterator_index];
FLATLAND_VERBOSE_LOG << "GlobalTopologyData processing current_entry=" << current_entry.handle
<< " child-count: " << current_entry.child_count;
++iterator_index;
// Mark that a child has been processed for the latest parent.
if (!parent_counts.empty()) {
FLATLAND_VERBOSE_LOG << "GlobalTopologyData parent_counts size: "
<< parent_counts.size()
<< " parent: " << topology_vector[parent_counts.back().parent_index]
<< " remaining-children: " << parent_counts.back().children_left;
FX_DCHECK(parent_counts.back().children_left > 0);
--parent_counts.back().children_left;
} else {
// Only expect to see this at the root of the topology, I think. Is this worth putting a
// permanent check in?
FLATLAND_VERBOSE_LOG << "GlobalTopologyData no parent";
}
// If we are processing a link transform, find the other end of the link (if it exists).
if (current_entry.handle.GetInstanceId() == link_instance_id) {
// Decrement the parent's child count until the link is successfully resolved. An unresolved
// link effectively means the parent had one fewer child.
FX_DCHECK(!parent_counts.empty());
auto& parent_child_count = child_counts[parent_counts.back().parent_index];
--parent_child_count;
// If the link doesn't exist, skip the link handle.
const auto link_kv = links.find(current_entry.handle);
if (link_kv == links.end()) {
FLATLAND_VERBOSE_LOG << "GlobalTopologyData link doesn't exist for handle "
<< current_entry.handle << ", skipping ";
if (parent_counts.back().children_left == 0) {
parent_counts.pop_back();
}
continue;
}
// If the link exists but doesn't have an UberStruct, skip the link handle.
const auto uber_struct_kv = uber_structs.find(link_kv->second.GetInstanceId());
if (uber_struct_kv == uber_structs.end()) {
FLATLAND_VERBOSE_LOG << "GlobalTopologyData link doesn't exist for instance_id "
<< link_kv->second.GetInstanceId() << ", skipping";
if (parent_counts.back().children_left == 0) {
parent_counts.pop_back();
}
continue;
}
// If the link exists and has an UberStruct but does not begin with the specified handle, skip
// the new topology. This can occur if a new UberStruct has not been registered for the
// corresponding instance ID but the link to it has resolved.
const auto& new_vector = uber_struct_kv->second->local_topology;
FX_DCHECK(!new_vector.empty()) << "Valid UberStructs cannot have empty local_topology";
if (new_vector[0].handle != link_kv->second) {
FLATLAND_VERBOSE_LOG << "GlobalTopologyData link mismatch with "
"existing UberStruct ("
<< new_vector[0].handle << " vs. " << link_kv->second << "), skipping";
if (parent_counts.back().children_left == 0) {
parent_counts.pop_back();
}
continue;
}
// Thanks to one-view-per-session semantics, we should never cycle through the
// topological vectors, so we don't need to handle cycles. We DCHECK here just to be sure.
FX_DCHECK(std::find_if(vector_stack.cbegin(), vector_stack.cend(),
[&](std::pair<const TransformGraph::TopologyVector&, uint64_t> entry) {
return entry.first == new_vector;
}) == vector_stack.cend());
// At this point, the link is resolved. This means the link did actually result in the parent
// having an additional child, but that child needs to be processed, so the stack of remaining
// children to process for each parent needs to be increment as well.
++parent_child_count;
++parent_counts.back().children_left;
vector_stack.emplace_back(new_vector, 0);
continue;
}
// Push the current transform and update the "iterator".
const size_t new_parent_index = topology_vector.size();
topology_vector.push_back(current_entry.handle);
// For each transform in the local topology, record its root.
root_transforms.emplace(current_entry.handle, vector[0].handle);
child_counts.push_back(current_entry.child_count);
parent_indices.push_back(parent_counts.empty() ? 0 : parent_counts.back().parent_index);
live_transforms.insert(current_entry.handle);
// For the root of each local topology (i.e. the View), save the ViewRef if it has one.
// Non-View roots might not have one, e.g. the display.
if (current_entry == vector[0] &&
uber_structs.at(current_entry.handle.GetInstanceId())->view_ref != nullptr) {
view_refs.emplace(current_entry.handle,
uber_structs.at(current_entry.handle.GetInstanceId())->view_ref);
}
// For the root of each local topology (i.e. the View), save the debug name if it is not empty.
if (current_entry == vector[0] &&
!uber_structs.at(current_entry.handle.GetInstanceId())->debug_name.empty()) {
debug_names.emplace(current_entry.handle,
uber_structs.at(current_entry.handle.GetInstanceId())->debug_name);
}
// For each node in the local topology, save the TransformClipRegion of its child instances.
for (auto& [child_handle, child_clip_region] :
uber_structs.at(current_entry.handle.GetInstanceId())->local_clip_regions) {
TransformClipRegion clip_region;
fidl::Clone(child_clip_region, &clip_region);
clip_regions.try_emplace(child_handle, std::move(clip_region));
}
// If this entry was the last child for the previous parent, pop that off the stack.
if (!parent_counts.empty() && parent_counts.back().children_left == 0) {
parent_counts.pop_back();
}
// If this entry has children, push it onto the parent stack.
if (current_entry.child_count != 0) {
parent_counts.push_back({new_parent_index, current_entry.child_count});
}
}
// Validates that every child of every parent was processed. If the last handle processed was an
// unresolved link handle, its parent will be the only thing left on the stack with 0 children to
// avoid extra unnecessary cleanup logic.
#ifndef NDEBUG
if (parent_counts.size() > 1 ||
(parent_counts.size() == 1 && parent_counts.back().children_left != 0)) {
std::ostringstream str;
str << "Error while generating GlobalTopologyData (failed parent_counts validation)\n"
<< "Dumping parent_counts vector:\n";
for (size_t i = 0; i < parent_counts.size(); ++i) {
str << "i: " << i << " index: " << parent_counts[i].parent_index
<< " parent: " << topology_vector[parent_counts[i].parent_index]
<< " child-count: " << parent_counts[i].children_left << std::endl;
}
FX_LOGS(FATAL) << str.str();
}
FX_CHECK(parent_counts.empty() ||
(parent_counts.size() == 1 && parent_counts.back().children_left == 0));
#endif
return {.topology_vector = std::move(topology_vector),
.child_counts = std::move(child_counts),
.parent_indices = std::move(parent_indices),
.live_handles = std::move(live_transforms),
.view_refs = std::move(view_refs),
.root_transforms = std::move(root_transforms),
.debug_names = std::move(debug_names),
.clip_regions = std::move(clip_regions)};
}
view_tree::SubtreeSnapshot GlobalTopologyData::GenerateViewTreeSnapshot(
const GlobalTopologyData& data, const std::unordered_set<zx_koid_t>& unconnected_view_refs,
const std::vector<TransformClipRegion>& global_clip_regions,
const std::unordered_map<TransformHandle, TransformHandle>& child_view_watcher_mapping) {
// Find the first node with a ViewRef set. This is the root of the ViewTree.
size_t root_index = 0;
while (root_index < data.topology_vector.size() &&
data.view_refs.count(data.topology_vector[root_index]) == 0) {
++root_index;
}
// Didn't find one -> empty ViewTree.
if (root_index == data.topology_vector.size()) {
return {};
}
view_tree::SubtreeSnapshot snapshot{// We do not currently support other compositors as subtrees.
.tree_boundaries = {}};
auto& [root, view_tree, unconnected_views, hit_tester, tree_boundaries] = snapshot;
// Add all Views to |view_tree|.
root = GetViewRefKoid(data.topology_vector[root_index], data.view_refs).value();
for (size_t i = root_index; i < data.topology_vector.size(); ++i) {
const auto& transform_handle = data.topology_vector.at(i);
if (data.view_refs.count(transform_handle) == 0) {
// Transforms without ViewRefs are not Views and can be skipped.
continue;
}
const auto& view_ref = data.view_refs.at(transform_handle);
const zx_koid_t view_ref_koid = utils::ExtractKoid(*view_ref);
std::string debug_name;
if (data.debug_names.count(transform_handle) != 0)
debug_name = data.debug_names.at(transform_handle);
// Find the parent by looking upwards until a View is found. The root has no parent.
// TODO(fxbug.dev/84196): Disallow anonymous views from having parents?
zx_koid_t parent_koid = ZX_KOID_INVALID;
if (view_ref_koid != root) {
size_t parent_index = data.parent_indices[i];
while (data.view_refs.count(data.topology_vector[parent_index]) == 0) {
parent_index = data.parent_indices[parent_index];
}
parent_koid = GetViewRefKoid(data.topology_vector[parent_index], data.view_refs).value();
}
// Set the coordinates of a view node's bounding box using the clip region coordinates stored
// in a view's parent uberstruct. The local clip region for a viewport is always identical to
// the local view boundary.
float max_width = 0, max_height = 0;
if (child_view_watcher_mapping.count(transform_handle) != 0) {
if (auto& parent_viewport_watcher_handle = child_view_watcher_mapping.at(transform_handle);
data.clip_regions.count(parent_viewport_watcher_handle) != 0) {
// Fetch the clip region coordinates for a view using its parent viewport watcher
// handle.
auto& clip_region = data.clip_regions.at(parent_viewport_watcher_handle);
max_width = clip_region.width;
max_height = clip_region.height;
}
}
// TODO(fxbug.dev/82678): Add local_from_world_transform to the ViewNode.
view_tree.emplace(
view_ref_koid,
view_tree::ViewNode{.parent = parent_koid,
.bounding_box = {.min = {0, 0}, .max = {max_width, max_height}},
.view_ref = view_ref,
.debug_name = debug_name});
}
// Fill in the children by deriving it from the parents of each node.
for (const auto& [koid, view_node] : view_tree) {
if (view_node.parent != ZX_KOID_INVALID) {
view_tree.at(view_node.parent).children.emplace(koid);
}
}
// Note: The ViewTree represents a snapshot of the scene at a specific time. Because of this it's
// important that it contains no references to live data. This means the hit testing closure must
// contain only plain values or data with value semantics like shared_ptr<const>, to ensure that
// it's safe to call from any thread.
hit_tester = [transforms = data.topology_vector, view_refs = data.view_refs,
parent_indices = data.parent_indices, hit_regions = data.hit_regions,
global_clip_regions, children_vector = data.child_counts,
root_transforms = data.root_transforms](zx_koid_t start_node, glm::vec2 world_point,
bool is_semantic_hit_test) {
FX_DCHECK(transforms.size() == parent_indices.size());
FX_DCHECK(transforms.size() == global_clip_regions.size());
FX_DCHECK(transforms.size() == children_vector.size());
size_t start = 0, end = 0;
if (auto result = GetViewRefIndex(start_node, transforms, view_refs)) {
start = *result;
end = GetSubtreeEndIndex(start, transforms, parent_indices);
} else {
return view_tree::SubtreeHitTestResult{};
}
FX_DCHECK(0 <= start && start < end && end <= transforms.size());
const auto x = world_point[0];
const auto y = world_point[1];
std::vector<zx_koid_t> hits = {};
for (size_t i = start; i < end; ++i) {
const auto& transform = transforms[i];
FX_DCHECK(root_transforms.find(transform) != root_transforms.end());
const auto& root_transform = root_transforms.at(transform);
const auto clip_region = utils::ConvertRectToRectF(global_clip_regions[i]);
if (const auto local_root = view_refs.find(root_transform); local_root != view_refs.end()) {
if (const auto hit_region_vec = hit_regions.find(transform);
hit_region_vec != hit_regions.end()) {
for (const auto& region : hit_region_vec->second) {
const auto rect = region.region;
const bool semantically_invisible =
region.hit_test ==
fuchsia::ui::composition::HitTestInteraction::SEMANTICALLY_INVISIBLE;
// Deliver a hit in all cases except for when it is a semantic hit test and the region
// is semantically invisible.
if (is_semantic_hit_test && semantically_invisible) {
continue;
}
// Instead of clipping the hit region with the clip region, simply check if the hit
// point is in both.
if (utils::RectFContainsPoint(rect, x, y) &&
utils::RectFContainsPoint(clip_region, x, y)) {
hits.push_back(utils::ExtractKoid(*(local_root->second)));
break;
}
}
}
}
}
std::reverse(hits.begin(), hits.end());
return view_tree::SubtreeHitTestResult{.hits = hits};
};
// Add unconnected views to the snapshot.
for (const auto view_ref_koid : unconnected_view_refs) {
if (view_tree.count(view_ref_koid) == 0) {
unconnected_views.insert(view_ref_koid);
}
}
return snapshot;
}
} // namespace flatland