blob: 4fd3f005cb3eb73fa92312923832a2ef33ad8e21 [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 <fuchsia/math/cpp/fidl.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <optional>
#include "src/ui/scenic/lib/flatland/flatland_types.h"
#include "src/ui/scenic/lib/utils/helpers.h"
#include "src/ui/scenic/lib/utils/logging.h"
#include <glm/gtc/type_ptr.hpp>
using flatland::GlobalTopologyData;
using flatland::TransformClipRegion;
using flatland::TransformHandle;
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 flatland::GlobalTopologyData::ViewRefMap& view_ref_map) {
const auto kv = view_ref_map.find(handle);
if (kv == view_ref_map.end() || std::get<0>(kv->second) == nullptr) {
return std::nullopt;
}
return std::get<1>(kv->second);
}
std::optional<size_t> GetViewRefIndex(zx_koid_t view_ref_koid,
const std::vector<flatland::TransformHandle>& transforms,
const flatland::GlobalTopologyData::ViewRefMap& view_refs) {
TRACE_DURATION("gfx", "flatland::HitTest[GetViewRefIndex]");
for (const auto& [transform, value] : view_refs) {
const auto& [view_ref, koid] = value;
if (view_ref != nullptr && view_ref_koid == koid) {
// 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) {
TRACE_DURATION("gfx", "flatland::HitTest[GetSubtreeEndIndex]");
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;
}
// Converts a 3x3 (2D) matrix to its 4x4 (3D) analog.
// xx, xy, yx, yy represent scale/rotation, T for translation.
//
// xx xy Tx
// yx yy TY
// 00 00 01
//
// ...becomes...
//
// xx xy 00 TX
// yx yy 00 TY
// 00 00 01 00
// 00 00 00 01
glm::mat4 Convert2DTransformTo3D(glm::mat3 in_matrix) {
// Construct identity matrix.
glm::mat4 out_matrix(1.f);
// Assign the rotation and scale values to the 2x2 submatrix.
out_matrix[0][0] = in_matrix[0][0];
out_matrix[0][1] = in_matrix[0][1];
out_matrix[1][0] = in_matrix[1][0];
out_matrix[1][1] = in_matrix[1][1];
// Assign the translation values to the final column.
out_matrix[3][0] = in_matrix[2][0];
out_matrix[3][1] = in_matrix[2][1];
return out_matrix;
}
// Wrapper function to check against implementation's sentinel value for infinite regions.
bool HitRegionContainsPoint(const flatland::HitRegion& region, float x, float y) {
if (!region.is_finite()) {
return true;
}
return region.region().Contains({.x = x, .y = y});
}
// Easier-to-read input data to HitTest() below.
struct HitTestingData {
const GlobalTopologyData::TopologyVector transforms;
const GlobalTopologyData::ParentIndexVector parent_indices;
const std::unordered_map<flatland::TransformHandle, flatland::TransformHandle> root_transforms;
const GlobalTopologyData::ViewRefMap view_refs;
const flatland::HitRegions hit_regions;
const std::vector<flatland::TransformClipRegion> global_clip_regions;
};
view_tree::SubtreeHitTestResult HitTest(const HitTestingData& data, zx_koid_t start_node,
glm::vec2 world_point, bool is_semantic_hit_test) {
TRACE_DURATION("gfx", "flatland::HitTest");
const auto& [transforms, parent_indices, root_transforms, view_refs, hit_regions,
global_clip_regions] = data;
FX_DCHECK(transforms.size() == parent_indices.size());
FX_DCHECK(transforms.size() == global_clip_regions.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 {
// Start node not in view tree.
return view_tree::SubtreeHitTestResult{};
}
FX_DCHECK(0 <= start && start < end && end <= transforms.size());
const float x = world_point[0];
const float y = world_point[1];
std::vector<zx_koid_t> hits = {};
hits.reserve(end - start);
for (size_t i = start; i < end; ++i) {
const auto& transform = transforms[i];
FX_DCHECK(root_transforms.contains(transform));
const auto& root_transform = root_transforms.at(transform);
const auto clip_region = types::RectangleF::From(global_clip_regions[i]);
// Skip anonymous views.
if (const auto local_root = view_refs.find(root_transform);
local_root != view_refs.end() && std::get<0>(local_root->second) != nullptr) {
const auto& [_, view_ref_koid] = local_root->second;
// Skip views without hit regions.
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 bool semantically_invisible =
region.interaction() ==
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 (HitRegionContainsPoint(region, x, y) && clip_region.Contains({.x = x, .y = y})) {
hits.push_back(view_ref_koid);
break;
}
}
}
}
}
std::reverse(hits.begin(), hits.end());
return view_tree::SubtreeHitTestResult{.hits = std::move(hits)};
}
// Returns whether the transform at |index| has an anonymous ancestor.
bool HasAnonymousAncestor(const size_t index, const size_t root_index,
const GlobalTopologyData::ViewRefMap& view_refs,
const GlobalTopologyData::TopologyVector& topology_vector,
const GlobalTopologyData::ParentIndexVector& parent_indices) {
if (index == root_index) {
return false;
}
size_t parent_index = parent_indices[index];
while (parent_index != root_index) {
// A transform that has an entry in the ViewRefMap is a view, but a nullptr entry is an
// anonymous view.
const auto parent_transform_handle = topology_vector[parent_index];
const auto it = view_refs.find(parent_transform_handle);
if (it != view_refs.end() && std::get<0>(it->second) == nullptr) {
return true;
}
parent_index = parent_indices[parent_index];
}
return false;
}
// Returns the index and ViewRef koid first node in the topology with a ViewRef set.
// If none is found it returns std::nullopt, indicating an empty ViewTree.
std::optional<std::pair<size_t, zx_koid_t>> FindRoot(
const GlobalTopologyData::TopologyVector& topology_vector,
const GlobalTopologyData::ViewRefMap& view_refs) {
for (size_t index = 0; index < topology_vector.size(); ++index) {
// TODO(https://fxbug.dev/42060740): Make sure the root view is not anonymous?
if (const auto koid = GetViewRefKoid(topology_vector[index], view_refs)) {
return std::make_pair(index, *koid);
}
}
return std::nullopt;
}
// Finds the parent of the node at |index| by looking upwards until a View is found.
// Returns ZX_KOID INVALID if no valid parent is found. (The root has no parent)
zx_koid_t FindParentView(const size_t index, const zx_koid_t view_ref_koid, const zx_koid_t root,
const GlobalTopologyData::TopologyVector& topology_vector,
const GlobalTopologyData::ParentIndexVector& parent_indices,
const GlobalTopologyData::ViewRefMap& view_refs) {
zx_koid_t parent_koid = ZX_KOID_INVALID;
if (view_ref_koid != root) {
size_t parent_index = parent_indices[index];
while (!view_refs.contains(topology_vector[parent_index])) {
parent_index = parent_indices[parent_index];
}
parent_koid = GetViewRefKoid(topology_vector[parent_index], view_refs).value();
}
return parent_koid;
}
// Returns the bounding box of |transform_handle| by findings the clip regions specified by its
// View's parent.
view_tree::BoundingBox ComputeBoundingBox(
const TransformHandle transform_handle,
const std::unordered_map<TransformHandle, TransformClipRegion>& clip_regions,
const std::unordered_map<TransformHandle, TransformHandle>&
link_child_to_parent_transform_map) {
std::array<float, 2> max_bounds = {0, 0};
if (const auto it = link_child_to_parent_transform_map.find(transform_handle);
it != link_child_to_parent_transform_map.end()) {
const TransformHandle parent_transform_handle = it->second;
if (const auto clip_region_it = clip_regions.find(parent_transform_handle);
clip_region_it != clip_regions.end()) {
const auto& extent = clip_region_it->second.extent();
max_bounds = {static_cast<float>(extent.width()), static_cast<float>(extent.height())};
}
}
return {.min = {0, 0}, .max = max_bounds};
}
// Return value struct for ComputeViewTree().
struct ViewTreeData {
std::unordered_map<zx_koid_t, view_tree::ViewNode> view_tree;
std::unordered_set<TransformHandle> implicitly_anonymous_views;
};
// Computes and returns the ViewTree plus a list of implicit anonymous views (named views that are
// part of an anonymous subtree) based on GlobalTopologyData.
ViewTreeData ComputeViewTree(
const zx_koid_t root, const size_t root_index,
const GlobalTopologyData::TopologyVector& topology_vector,
const GlobalTopologyData::ParentIndexVector& parent_indices,
const GlobalTopologyData::ViewRefMap& view_refs,
const std::unordered_map<TransformHandle, std::string>& debug_names,
const std::unordered_map<TransformHandle, TransformClipRegion>& global_clip_regions,
const std::vector<glm::mat3>& global_matrix_vector,
const std::unordered_map<TransformHandle, TransformHandle>&
link_child_to_parent_transform_map) {
TRACE_DURATION("gfx", "flatland::ComputeViewTree");
ViewTreeData output;
for (size_t i = root_index; i < topology_vector.size(); ++i) {
const auto& transform_handle = topology_vector.at(i);
const auto view_ref_it = view_refs.find(transform_handle);
// Transforms without ViewRefs are not Views and can be skipped.
if (view_ref_it == view_refs.end()) {
continue;
}
const auto& [view_ref, view_ref_koid] = view_ref_it->second;
// Anonymous views can be skipped.
if (view_ref == nullptr) {
continue;
}
// If any node in the ancestor chain is anonymous then the View should marked "unconnected".
if (HasAnonymousAncestor(i, root_index, view_refs, topology_vector, parent_indices)) {
output.implicitly_anonymous_views.emplace(transform_handle);
continue;
}
std::string debug_name;
if (auto debug_it = debug_names.find(transform_handle); debug_it != debug_names.end()) {
debug_name = debug_it->second;
}
const zx_koid_t parent_koid =
FindParentView(i, view_ref_koid, root, topology_vector, parent_indices, view_refs);
const view_tree::BoundingBox bounding_box = ComputeBoundingBox(
transform_handle, global_clip_regions, link_child_to_parent_transform_map);
output.view_tree.emplace(
view_ref_koid, view_tree::ViewNode{.parent = parent_koid,
.bounding_box = bounding_box,
.local_from_world_transform = glm::inverse(
Convert2DTransformTo3D(global_matrix_vector[i])),
.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] : output.view_tree) {
if (view_node.parent != ZX_KOID_INVALID) {
output.view_tree.at(view_node.parent).children.emplace(koid);
}
}
return output;
}
} // namespace
namespace flatland {
// static
GlobalTopologyData GlobalTopologyData::ComputeGlobalTopologyData(
const UberStruct::InstanceMap& uber_structs, const LinkTopologyMap& links,
TransformHandle::InstanceId link_instance_id, TransformHandle root) {
TRACE_DURATION("gfx", "flatland::ComputeGlobalTopologyData");
// There should never be an UberStruct for the |link_instance_id|.
FX_DCHECK(!uber_structs.contains(link_instance_id));
#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;
GlobalTopologyData output;
auto& [topology_vector, child_counts, parent_indices, live_transforms, view_refs, root_transforms,
debug_names, clip_regions] = output;
// For the root of each local topology (i.e. the View), save the ViewRef, whether they're
// currently attached to the scene or not.
for (const auto& [_, uber_struct] : uber_structs) {
if (!uber_struct->local_topology.empty()) {
zx_koid_t view_ref_koid = uber_struct->view_ref == nullptr
? ZX_KOID_INVALID
: utils::ExtractKoid(*uber_struct->view_ref);
view_refs.emplace(uber_struct->local_topology[0].handle,
std::make_tuple(uber_struct->view_ref, view_ref_koid));
}
}
// 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;
}
const TransformHandle& link_transform = link_kv->second;
// If the link exists but doesn't have an UberStruct, skip the link handle.
const auto uber_struct_kv = uber_structs.find(link_transform.GetInstanceId());
if (uber_struct_kv == uber_structs.end()) {
FLATLAND_VERBOSE_LOG << "GlobalTopologyData link doesn't exist for instance_id="
<< link_transform.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_transform) {
FLATLAND_VERBOSE_LOG << "GlobalTopologyData link mismatch with "
"existing UberStruct ("
<< new_vector[0].handle << " vs. " << link_transform << "), 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 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) {
clip_regions.try_emplace(child_handle, child_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 output;
}
view_tree::SubtreeSnapshot GlobalTopologyData::GenerateViewTreeSnapshot(
const GlobalTopologyData& data, HitRegions hit_regions,
std::vector<TransformClipRegion> global_clip_regions,
const std::vector<glm::mat3>& global_matrix_vector,
const std::unordered_map<TransformHandle, TransformHandle>&
link_child_to_parent_transform_map) {
TRACE_DURATION("gfx", "flatland::GenerateViewTreeSnapshot");
const auto root_values = FindRoot(data.topology_vector, data.view_refs);
if (!root_values.has_value()) {
// No root -> Empty ViewTree.
return {};
}
view_tree::SubtreeSnapshot output;
auto& [root, view_tree, unconnected_views, hit_tester, _] = output;
const auto [root_index, root_koid] = root_values.value();
root = root_koid;
auto [view_tree_temp, implicitly_anonymous_views] =
ComputeViewTree(root_koid, root_index, data.topology_vector, data.parent_indices,
data.view_refs, data.debug_names, data.clip_regions, global_matrix_vector,
link_child_to_parent_transform_map);
view_tree = std::move(view_tree_temp);
// Unconnected_views = all non-anonymous views (those with ViewRefs) not in the ViewTree.
for (const auto& [_, value] : data.view_refs) {
auto& [view_ref, view_ref_koid] = value;
if (view_ref != nullptr) {
if (!view_tree.contains(view_ref_koid)) {
unconnected_views.emplace(view_ref_koid);
}
}
}
// Copy all non-anonymous ViewRefs from the ViewRefMap.
ViewRefMap named_view_refs;
named_view_refs.reserve(data.view_refs.size());
for (auto& [key, value] : data.view_refs) {
if (!implicitly_anonymous_views.contains(key)) {
named_view_refs.emplace(key, value);
}
}
// 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 = [hit_test_data = HitTestingData{
.transforms = data.topology_vector,
.parent_indices = data.parent_indices,
.root_transforms = data.root_transforms,
.view_refs = std::move(named_view_refs),
.hit_regions = std::move(hit_regions),
.global_clip_regions = std::move(global_clip_regions),
}](zx_koid_t start_node, glm::vec2 world_point, bool is_semantic_hit_test) {
return HitTest(hit_test_data, start_node, world_point, is_semantic_hit_test);
};
return output;
}
} // namespace flatland