blob: c5afb767c01e949e9da86eba729747330ab6396a [file] [log] [blame]
// Copyright 2019 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/flatland.h"
#include <lib/async/default.h>
#include <lib/async/time.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <lib/ui/scenic/cpp/view_identity.h>
#include <lib/zx/eventpair.h>
#include <limits.h>
#include <functional>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "src/lib/fostr/fidl/fuchsia.math/amendments.h"
#include "src/lib/fsl/handles/object_info.h"
#include "src/ui/scenic/lib/allocation/id.h"
#include "src/ui/scenic/lib/flatland/flatland_types.h"
#include "src/ui/scenic/lib/scheduling/id.h"
#include "src/ui/scenic/lib/utils/helpers.h"
#include "src/ui/scenic/lib/utils/logging.h"
#include "src/ui/scenic/lib/utils/validate_eventpair.h"
#include "zircon/errors.h"
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/type_ptr.hpp>
using fuchsia::math::RectF;
using fuchsia::math::SizeU;
using fuchsia::math::Vec;
using fuchsia::math::VecF;
using fuchsia::ui::composition::ChildViewStatus;
using fuchsia::ui::composition::ChildViewWatcher;
using fuchsia::ui::composition::FlatlandError;
using fuchsia::ui::composition::HitRegion;
using fuchsia::ui::composition::ImageProperties;
using fuchsia::ui::composition::OnNextFrameBeginValues;
using fuchsia::ui::composition::Orientation;
using fuchsia::ui::composition::ParentViewportWatcher;
using fuchsia::ui::composition::ViewportProperties;
using fuchsia::ui::views::ViewCreationToken;
using fuchsia::ui::views::ViewportCreationToken;
namespace {
// Handle floating point errors up to an epsilon for sample region calls.
void ClampIfNear(float* val, float difference) {
if (difference > 0.f && difference < 1e-3f) {
*val -= difference;
}
}
std::optional<std::string> ValidateViewportProperties(const ViewportProperties& properties) {
if (properties.has_logical_size()) {
const auto logical_size = properties.logical_size();
if (logical_size.width == 0 || logical_size.height == 0) {
std::ostringstream stream;
stream << "Logical_size components must be positive, given (" << logical_size.width << ", "
<< logical_size.height << ")";
return stream.str();
}
}
if (properties.has_inset()) {
const auto inset = properties.inset();
if (inset.top < 0 || inset.right < 0 || inset.bottom < 0 || inset.left < 0) {
std::ostringstream stream;
stream << "Inset components must be >= 0, given (" << inset.top << ", " << inset.right << ", "
<< inset.bottom << ", " << inset.left << ")";
return stream.str();
}
}
return std::nullopt;
}
void SetViewportPropertiesMissingDefaults(ViewportProperties& properties,
const fuchsia::math::SizeU& logical_size,
const fuchsia::math::Inset& inset) {
if (!properties.has_logical_size()) {
properties.set_logical_size(logical_size);
}
if (!properties.has_inset()) {
properties.set_inset(inset);
}
}
} // namespace
namespace flatland {
std::shared_ptr<Flatland> Flatland::New(
std::shared_ptr<utils::DispatcherHolder> dispatcher_holder,
fidl::InterfaceRequest<fuchsia::ui::composition::Flatland> request,
scheduling::SessionId session_id, std::function<void()> destroy_instance_function,
std::shared_ptr<FlatlandPresenter> flatland_presenter, std::shared_ptr<LinkSystem> link_system,
std::shared_ptr<UberStructSystem::UberStructQueue> uber_struct_queue,
const std::vector<std::shared_ptr<allocation::BufferCollectionImporter>>&
buffer_collection_importers,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::views::Focuser>, zx_koid_t)>
register_view_focuser,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::views::ViewRefFocused>, zx_koid_t)>
register_view_ref_focused,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::pointer::TouchSource>, zx_koid_t)>
register_touch_source,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::pointer::MouseSource>, zx_koid_t)>
register_mouse_source) {
// clang-format off
return std::shared_ptr<Flatland>(new Flatland(
std::move(dispatcher_holder),
std::move(request), session_id,
std::move(destroy_instance_function),
std::move(flatland_presenter),
std::move(link_system),
std::move(uber_struct_queue),
buffer_collection_importers,
std::move(register_view_focuser),
std::move(register_view_ref_focused),
std::move(register_touch_source),
std::move(register_mouse_source)));
// clang-format on
}
Flatland::Flatland(
std::shared_ptr<utils::DispatcherHolder> dispatcher_holder,
fidl::InterfaceRequest<fuchsia::ui::composition::Flatland> request,
scheduling::SessionId session_id, std::function<void()> destroy_instance_function,
std::shared_ptr<FlatlandPresenter> flatland_presenter, std::shared_ptr<LinkSystem> link_system,
std::shared_ptr<UberStructSystem::UberStructQueue> uber_struct_queue,
const std::vector<std::shared_ptr<allocation::BufferCollectionImporter>>&
buffer_collection_importers,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::views::Focuser>, zx_koid_t)>
register_view_focuser,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::views::ViewRefFocused>, zx_koid_t)>
register_view_ref_focused,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::pointer::TouchSource>, zx_koid_t)>
register_touch_source,
fit::function<void(fidl::InterfaceRequest<fuchsia::ui::pointer::MouseSource>, zx_koid_t)>
register_mouse_source)
: dispatcher_holder_(std::move(dispatcher_holder)),
binding_(this, std::move(request), dispatcher_holder_->dispatcher()),
session_id_(session_id),
destroy_instance_function_(std::move(destroy_instance_function)),
peer_closed_waiter_(binding_.channel().get(), ZX_CHANNEL_PEER_CLOSED),
present2_helper_([this](fuchsia::scenic::scheduling::FramePresentedInfo info) {
if (binding_.is_bound()) {
binding_.events().OnFramePresented(std::move(info));
}
}),
flatland_presenter_(std::move(flatland_presenter)),
link_system_(std::move(link_system)),
uber_struct_queue_(std::move(uber_struct_queue)),
buffer_collection_importers_(buffer_collection_importers),
transform_graph_(session_id_),
local_root_(transform_graph_.CreateTransform()),
error_reporter_(scenic_impl::ErrorReporter::DefaultUnique()),
images_to_release_(std::make_shared<std::unordered_set<allocation::GlobalImageId>>()),
register_view_focuser_(std::move(register_view_focuser)),
register_view_ref_focused_(std::move(register_view_ref_focused)),
register_touch_source_(std::move(register_touch_source)),
register_mouse_source_(std::move(register_mouse_source)) {
FX_DCHECK(flatland_presenter_);
zx_status_t status = peer_closed_waiter_.Begin(
dispatcher(), [this](async_dispatcher_t* dispatcher, async::WaitOnce* wait,
zx_status_t status, const zx_packet_signal_t* signal) {
if (!destroy_instance_function_was_invoked_) {
destroy_instance_function_was_invoked_ = true;
destroy_instance_function_();
}
});
FX_DCHECK(status == ZX_OK);
FLATLAND_VERBOSE_LOG << "Flatland new with ID: " << session_id_;
}
Flatland::~Flatland() {
// TODO(https://fxbug.dev/42132996): consider if Link tokens should be returned or not.
// Clear the scene graph, then collect the images to release.
Clear();
auto data = transform_graph_.ComputeAndCleanup(GetRoot(), std::numeric_limits<uint64_t>::max());
// We don't care about the images returned by `ProcessDeadTransforms()` because we want to release
// all the images in `images_to_release_`, which potentially includes some added by
// `ProcessDeadTransforms()`.
ProcessDeadTransforms(data);
FX_DCHECK(image_metadatas_.empty());
// If there are any images to release, set up a waiter, and pass the event-to-be-signaled to
// `FlatlandPresenter::RemoveSession`. This will schedule another frame and signal the event
// just like any other release fence.
std::optional<zx::event> image_release_fence;
if (!images_to_release_->empty()) {
zx::event evt = utils::CreateEvent();
image_release_fence = utils::CopyEvent(evt);
auto wait = std::make_shared<async::WaitOnce>(evt.get(), ZX_EVENT_SIGNALED);
zx_status_t status = wait->Begin(
dispatcher(),
[importer_refs = buffer_collection_importers_, images_to_release = images_to_release_,
// We keep several objects alive in the closure:
// - the dispatcher, which is about to be released by the Flatland and FlatlandManager.
// - the wait object keeps itself alive via the ref in this closure
// - the waited-upon fence event: we retain a copy of the handle to avoid reasoning about
// whether the FlatlandPresenter implementation will safely keep it alive.
keepalive_dispatcher = dispatcher_holder_, keepalive_wait = wait,
keepalive_evt = std::move(evt)](async_dispatcher_t*, async::WaitOnce*, zx_status_t status,
const zx_packet_signal_t* /*signal*/) mutable {
for (auto& image_id : *images_to_release) {
for (auto& importer : importer_refs) {
importer->ReleaseBufferImage(image_id);
}
}
images_to_release->clear();
});
FX_DCHECK(status == ZX_OK);
}
// This will signal the release fence (if any) that we pass to it, and therefore enable the wait
// above to succeed.
flatland_presenter_->RemoveSession(session_id_, std::move(image_release_fence));
}
void Flatland::Present(fuchsia::ui::composition::PresentArgs args) {
TRACE_DURATION("gfx", "Flatland::Present", "debug_name", TA_STRING(debug_name_.c_str()));
std::string per_app_tracing_name = "Flatland::PerAppPresent[" + debug_name_ + "]";
TRACE_DURATION("gfx", per_app_tracing_name.c_str());
TRACE_FLOW_END("gfx", per_app_tracing_name.c_str(), present_count_);
++present_count_;
FLATLAND_VERBOSE_LOG << "Flatland::Present() #" << present_count_ << " for " << local_root_ << " "
<< this;
// Close any clients that had invalid operations on link protocols.
if (link_protocol_error_) {
error_reporter_->ERROR() << "Link protocol error";
CloseConnection(FlatlandError::BAD_HANGING_GET);
return;
}
// Close any clients that call Present() without any present tokens.
if (present_credits_ == 0) {
error_reporter_->ERROR() << "Out of present credits";
CloseConnection(FlatlandError::NO_PRESENTS_REMAINING);
return;
}
present_credits_--;
// If any fields are missing, replace them with the default values.
if (!args.has_requested_presentation_time()) {
args.set_requested_presentation_time(0);
}
if (!args.has_release_fences()) {
args.set_release_fences({});
}
if (!args.has_acquire_fences()) {
args.set_acquire_fences({});
}
if (!args.has_unsquashable()) {
args.set_unsquashable(false);
}
auto root_handle = GetRoot();
// TODO(https://fxbug.dev/42116832): Decide on a proper limit on compute time for topological
// sorting.
auto data = transform_graph_.ComputeAndCleanup(root_handle, std::numeric_limits<uint64_t>::max());
FX_DCHECK(data.iterations != std::numeric_limits<uint64_t>::max());
// TODO(https://fxbug.dev/42111664): Once the 2D scene graph is externalized, don't commit changes
// if a cycle is detected. Instead, kill the channel and remove the sub-graph from the global
// graph.
failure_since_previous_present_ |= !data.cyclical_edges.empty();
if (failure_since_previous_present_) {
CloseConnection(FlatlandError::BAD_OPERATION);
return;
}
FX_DCHECK(data.sorted_transforms[0].handle == root_handle);
// Cleanup released resources. Here we also collect the list of unused images so they can be
// released by the buffer collection importers.
auto images_to_release = ProcessDeadTransforms(data);
// If there are images ready for release, create a release fence for the current Present() and
// delay release until that fence is reached to ensure that the images are no longer referenced
// in any render data.
if (!images_to_release.empty()) {
// Create a release fence specifically for the images.
zx::event image_release_fence;
zx_status_t status = zx::event::create(0, &image_release_fence);
FX_DCHECK(status == ZX_OK);
// Use a self-referencing async::WaitOnce to perform ImageImporter deregistration.
// This is primarily so the handler does not have to live in the Flatland instance, which may
// be destroyed before the release fence is signaled. `WaitOnce` moves the handler to the stack
// prior to invoking it, so it is safe for the handler to delete the WaitOnce on exit.
// Specifically, we move the wait object into the lambda function via |copy_ref = wait| to
// ensure that the wait object lives. The callback will not trigger without this.
auto wait = std::make_shared<async::WaitOnce>(image_release_fence.get(), ZX_EVENT_SIGNALED);
status = wait->Begin(
dispatcher(),
[copy_ref = wait, importer_refs = buffer_collection_importers_, images_to_release,
all_images_to_release = images_to_release_,
session_id = session_id_](async_dispatcher_t*, async::WaitOnce*, zx_status_t status,
const zx_packet_signal_t* /*signal*/) mutable {
// The wait is canceled if the dispatcher is destroyed before the event is signaled.
// In this case, we expect the images to have already been released by the wait in the
// ~Flatland() destructor.
FX_DCHECK(status == ZX_OK || status == ZX_ERR_CANCELED)
<< "status is: " << zx_status_get_string(status) << " (" << status << ")";
if (status == ZX_ERR_CANCELED) {
FX_DCHECK(all_images_to_release->empty());
return;
}
for (auto& image_id : images_to_release) {
if (!all_images_to_release->erase(image_id)) {
// This is harmless, but typically shouldn't happen. The rare exception is a race
// when the Flatland session is being torn down, if this runs after the session is
// destroyed, but before the session's loop/thread is stopped.
FX_LOGS(WARNING) << "Flatland session << " << session_id
<< " did not find expected image " << image_id
<< " in images_to_release_";
continue;
}
for (auto& importer : importer_refs) {
importer->ReleaseBufferImage(image_id);
}
}
});
FX_DCHECK(status == ZX_OK) << "status is: " << status;
// Push the new release fence into the user-provided list.
args.mutable_release_fences()->push_back(std::move(image_release_fence));
}
auto uber_struct = std::make_unique<UberStruct>();
uber_struct->local_topology = std::move(data.sorted_transforms);
for (const auto& [handle, matrix_data] : matrices_) {
uber_struct->local_matrices[handle] = matrix_data.GetMatrix();
}
for (const auto& [handle, sample_region] : image_sample_regions_) {
uber_struct->local_image_sample_regions[handle] = sample_region;
}
for (const auto& [handle, opacity_value] : opacity_values_) {
uber_struct->local_opacity_values[handle] = opacity_value;
}
for (const auto& [handle, clip_region] : clip_regions_) {
uber_struct->local_clip_regions[handle] = clip_region;
}
for (const auto& [handle, hit_regions] : hit_regions_) {
uber_struct->local_hit_regions_map[handle] = hit_regions;
}
// As per the default hit region policy, if the client has not explicitly set a hit region on the
// root, add a full screen one.
if (root_transform_.GetInstanceId() != 0 &&
hit_regions_.find(root_transform_) == hit_regions_.end()) {
uber_struct->local_hit_regions_map[root_transform_] = {{flatland::HitRegion::Infinite()}};
}
uber_struct->images = image_metadatas_;
if (link_to_parent_.has_value()) {
uber_struct->view_ref = link_to_parent_->view_ref;
}
uber_struct->debug_name = debug_name_;
// Obtain the PresentId which is needed to:
// - enqueue the UberStruct.
// - schedule a frame
// - notify client when the frame has been presented
auto present_id = scheduling::GetNextPresentId();
present2_helper_.RegisterPresent(present_id,
/*present_received_time=*/zx::time(async_now(dispatcher())));
TRACE_FLOW_BEGIN("gfx", "ScheduleUpdate", present_id);
TRACE_FLOW_BEGIN("gfx", "wait_for_fences", SESSION_TRACE_ID(session_id_, present_id));
// Safe to capture |this| because the Flatland is guaranteed to outlive |fence_queue_|,
// Flatland is non-movable and FenceQueue does not fire closures after destruction.
// TODO(https://fxbug.dev/42156567): make the fences be the first arg, and the closure be the
// second.
fence_queue_->QueueTask(
[this, present_id, requested_presentation_time = args.requested_presentation_time(),
unsquashable = args.unsquashable(), uber_struct = std::move(uber_struct),
link_operations = std::move(pending_link_operations_),
release_fences = std::move(*args.mutable_release_fences())]() mutable {
// NOTE: this name is important for benchmarking. Do not remove or modify it
// without also updating the "process_gfx_trace.go" script.
TRACE_DURATION("gfx", "scenic_impl::Session::ScheduleNextPresent", "session_id",
session_id_, "requested_presentation_time", requested_presentation_time);
TRACE_FLOW_END("gfx", "wait_for_fences", SESSION_TRACE_ID(session_id_, present_id));
// Push the UberStruct, then schedule the associated Present that will eventually publish
// it to the InstanceMap used for rendering.
uber_struct_queue_->Push(present_id, std::move(uber_struct));
flatland_presenter_->ScheduleUpdateForSession(zx::time(requested_presentation_time),
{session_id_, present_id}, unsquashable,
std::move(release_fences));
// Finalize Link destruction operations after publishing the new UberStruct. This
// ensures that any local Transforms referenced by the to-be-deleted Links are already
// removed from the now-published UberStruct.
for (auto& operation : link_operations) {
operation();
}
},
std::move(*args.mutable_acquire_fences()));
// We exited early in this method if there was a failure, and none of the subsequent operations
// are allowed to trigger a failure (all failure possibilities should be checked before the
// early exit).
FX_DCHECK(!failure_since_previous_present_);
}
void Flatland::CreateView(ViewCreationToken token,
fidl::InterfaceRequest<ParentViewportWatcher> parent_viewport_watcher) {
TRACE_DURATION("gfx", "Flatland::CreateView", "debug_name", TA_STRING(debug_name_.c_str()));
CreateViewHelper(std::move(token), std::move(parent_viewport_watcher), std::nullopt,
std::nullopt);
}
void Flatland::CreateView2(ViewCreationToken token,
fuchsia::ui::views::ViewIdentityOnCreation view_identity,
fuchsia::ui::composition::ViewBoundProtocols protocols,
fidl::InterfaceRequest<ParentViewportWatcher> parent_viewport_watcher) {
TRACE_DURATION("gfx", "Flatland::CreateView2", "debug_name", TA_STRING(debug_name_.c_str()));
CreateViewHelper(std::move(token), std::move(parent_viewport_watcher), std::move(view_identity),
std::move(protocols));
}
void Flatland::CreateViewHelper(
ViewCreationToken token, fidl::InterfaceRequest<ParentViewportWatcher> parent_viewport_watcher,
std::optional<fuchsia::ui::views::ViewIdentityOnCreation> view_identity,
std::optional<fuchsia::ui::composition::ViewBoundProtocols> protocols) {
// Attempting to link with an invalid token will never succeed, so its better to fail early and
// immediately close the link connection.
if (!token.value.is_valid()) {
error_reporter_->ERROR() << "CreateView failed, ViewCreationToken was invalid";
ReportBadOperationError();
return;
}
if (view_identity.has_value() &&
!utils::validate_viewref(view_identity->view_ref_control, view_identity->view_ref)) {
error_reporter_->ERROR() << "CreateView failed, ViewIdentityOnCreation was invalid";
ReportBadOperationError();
return;
}
FX_DCHECK(link_system_);
if (protocols.has_value()) {
FX_DCHECK(view_identity.has_value()) << "required for view-bound protocols";
RegisterViewBoundProtocols(std::move(*protocols), utils::ExtractKoid(view_identity->view_ref));
}
// This portion of the method is not feed forward. This makes it possible for clients to receive
// layout information before this operation has been presented. By initializing the link
// immediately, parents can inform children of layout changes, and child clients can perform
// layout decisions before their first call to Present().
auto child_transform_handle = transform_graph_.CreateTransform();
LinkSystem::LinkToParent new_link_to_parent = link_system_->CreateLinkToParent(
dispatcher_holder_, std::move(token), std::move(view_identity),
std::move(parent_viewport_watcher), child_transform_handle,
[ref = weak_from_this(), weak_dispatcher_holder = std::weak_ptr<utils::DispatcherHolder>(
dispatcher_holder_)](const std::string& error_log) {
if (auto dispatcher_holder = weak_dispatcher_holder.lock()) {
FX_CHECK(dispatcher_holder->dispatcher() == async_get_default_dispatcher())
<< "Link protocol error reported on the wrong dispatcher.";
}
if (auto impl = ref.lock())
impl->ReportLinkProtocolError(error_log);
});
FLATLAND_VERBOSE_LOG << "Flatland::CreateView() link-attachment-point: "
<< child_transform_handle;
// This portion of the method is feed-forward. The parent-child relationship between
// |child_transform_handle| and |local_root_| establishes the Transform hierarchy between the two
// instances, but the operation will not be visible until the next Present() call includes that
// topology.
if (link_to_parent_.has_value()) {
bool child_removed =
transform_graph_.RemoveChild(link_to_parent_->child_transform_handle, local_root_);
FX_DCHECK(child_removed);
bool transform_released =
transform_graph_.ReleaseTransform(link_to_parent_->child_transform_handle);
FX_DCHECK(transform_released);
// Delay the destruction of the previous parent link until the next Present().
pending_link_operations_.push_back([old_link_to_parent = std::move(link_to_parent_)]() mutable {
old_link_to_parent.reset();
});
}
{
const bool child_added =
transform_graph_.AddChild(new_link_to_parent.child_transform_handle, local_root_);
FX_DCHECK(child_added);
}
link_to_parent_ = std::move(new_link_to_parent);
}
void Flatland::RegisterViewBoundProtocols(fuchsia::ui::composition::ViewBoundProtocols protocols,
const zx_koid_t view_ref_koid) {
FX_DCHECK(register_view_focuser_);
FX_DCHECK(register_view_ref_focused_);
FX_DCHECK(register_touch_source_);
FX_DCHECK(register_mouse_source_);
if (protocols.has_view_focuser()) {
register_view_focuser_(std::move(*protocols.mutable_view_focuser()), view_ref_koid);
}
if (protocols.has_view_ref_focused()) {
register_view_ref_focused_(std::move(*protocols.mutable_view_ref_focused()), view_ref_koid);
}
if (protocols.has_touch_source()) {
register_touch_source_(std::move(*protocols.mutable_touch_source()), view_ref_koid);
}
if (protocols.has_mouse_source()) {
register_mouse_source_(std::move(*protocols.mutable_mouse_source()), view_ref_koid);
}
}
void Flatland::ReleaseView() {
if (!link_to_parent_) {
error_reporter_->ERROR() << "ReleaseView failed, no existing parent Link";
ReportBadOperationError();
return;
}
// Deleting the old LinkToParent's Transform effectively changes this intance's root back to
// |local_root_|.
bool child_removed =
transform_graph_.RemoveChild(link_to_parent_->child_transform_handle, local_root_);
FX_DCHECK(child_removed);
bool transform_released =
transform_graph_.ReleaseTransform(link_to_parent_->child_transform_handle);
FX_DCHECK(transform_released);
// Move the old parent link into the delayed operation so that it isn't taken into account when
// computing the local topology, but doesn't get deleted until after the new UberStruct is
// published.
auto old_link_to_parent = std::move(link_to_parent_.value());
link_to_parent_.reset();
// Delay the actual destruction of the Link until the next Present().
pending_link_operations_.push_back([old_link_to_parent = std::move(old_link_to_parent)]() {});
}
void Flatland::Clear() {
// Clear user-defined mappings and local matrices.
transforms_.clear();
content_handles_.clear();
matrices_.clear();
// We always preserve the link origin when clearing the graph. This call will place all other
// TransformHandles in the dead_transforms set in the next Present(), which will trigger cleanup
// of Images and BufferCollections.
transform_graph_.ResetGraph(local_root_);
// If a parent Link exists, delay its destruction until Present().
if (link_to_parent_.has_value()) {
auto local_link = std::move(link_to_parent_);
link_to_parent_.reset();
pending_link_operations_.push_back(
[local_link = std::move(local_link)]() mutable { local_link.reset(); });
}
// Delay destruction of all child Links until Present().
auto local_links = std::move(links_to_children_);
links_to_children_.clear();
pending_link_operations_.push_back(
[local_links = std::move(local_links)]() mutable { local_links.clear(); });
debug_name_.clear();
}
void Flatland::CreateTransform(TransformId transform_id) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "CreateTransform called with transform_id 0";
ReportBadOperationError();
return;
}
if (transforms_.count(transform_id.value)) {
error_reporter_->ERROR() << "CreateTransform called with pre-existing transform_id "
<< transform_id.value;
ReportBadOperationError();
return;
}
TransformHandle handle = transform_graph_.CreateTransform();
FLATLAND_VERBOSE_LOG << "Flatland::CreateTransform() client-id: " << transform_id.value
<< " handle: " << handle;
transforms_.insert({transform_id.value, handle});
}
void Flatland::SetTranslation(TransformId transform_id, Vec translation) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetTranslation called with transform_id 0";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetTranslation failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
matrices_[transform_kv->second].SetTranslation(translation);
}
void Flatland::SetOrientation(TransformId transform_id, Orientation orientation) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetOrientation called with transform_id 0";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetOrientation failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
matrices_[transform_kv->second].SetOrientation(orientation);
}
void Flatland::SetScale(TransformId transform_id, VecF scale) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetScale called with transform_id 0";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetScale failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
if (scale.x == 0.f || scale.y == 0.f) {
error_reporter_->ERROR() << "SetScale failed, zero values not allowed (" << scale.x << ", "
<< scale.y << " ).";
ReportBadOperationError();
return;
}
if (isinf(scale.x) || isinf(scale.y) || isnan(scale.x) || isnan(scale.y)) {
error_reporter_->ERROR() << "SetScale failed, invalid scale values (" << scale.x << ", "
<< scale.y << " ).";
ReportBadOperationError();
return;
}
matrices_[transform_kv->second].SetScale(scale);
}
void Flatland::SetOpacity(TransformId transform_id, float value) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetOpacity called with transform_id 0";
ReportBadOperationError();
return;
}
if (isinf(value) || isnan(value)) {
error_reporter_->ERROR() << "SetOpacity failed, invalid opacity value " << value;
ReportBadOperationError();
return;
}
if (value < 0.f || value > 1.f) {
error_reporter_->ERROR() << "Opacity value is not within valid range [0,1].";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetOpacity failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
// Erase the value from the map since we store 1.f implicity.
if (value == 1.f) {
opacity_values_.erase(transform_kv->second);
} else {
opacity_values_[transform_kv->second] = value;
}
}
void Flatland::SetClipBoundary(TransformId transform_id,
std::unique_ptr<fuchsia::math::Rect> bounds_ptr) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetClipBoundary called with transform_id 0";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetClipBoundary failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
// If the optional bounds are empty, then remove them.
if (!bounds_ptr) {
clip_regions_.erase(transform_kv->second);
return;
}
SetClipBoundaryInternal(transform_kv->second, *bounds_ptr.get());
}
void Flatland::SetClipBoundaryInternal(TransformHandle handle, fuchsia::math::Rect bounds) {
if (bounds.width <= 0 || bounds.height <= 0) {
error_reporter_->ERROR() << "SetClipBoundary failed, width/height must both be positive " << "("
<< bounds.width << ", " << bounds.height << ")";
ReportBadOperationError();
return;
}
// The following overflow checks are based on those described here:
// https://wiki.sei.cmu.edu/confluence/display/c/INT32-C.
// +Ensure+that+operations+on+signed+integers+do+not+result+in+overflow
if (((bounds.x > 0) && (bounds.width > (INT_MAX - bounds.x))) ||
((bounds.x < 0) && (bounds.width < (INT_MIN - bounds.x)))) {
error_reporter_->ERROR() << "SetClipBoundary failed, integer overflow on the X-axis.";
ReportBadOperationError();
return;
}
if (((bounds.y > 0) && (bounds.height > (INT_MAX - bounds.y))) ||
((bounds.y < 0) && (bounds.height < (INT_MIN - bounds.y)))) {
error_reporter_->ERROR() << "SetClipBoundary failed, integer overflow on the Y-axis.";
ReportBadOperationError();
return;
}
clip_regions_[handle] = bounds;
}
std::vector<allocation::GlobalImageId> Flatland::ProcessDeadTransforms(
const TransformGraph::TopologyData& data) {
std::vector<allocation::GlobalImageId> images_to_release;
for (const auto& dead_handle : data.dead_transforms) {
matrices_.erase(dead_handle);
// Gather all images corresponding to dead transforms.
auto image_kv = image_metadatas_.find(dead_handle);
if (image_kv != image_metadatas_.end()) {
const auto image_id = image_kv->second.identifier;
image_metadatas_.erase(image_kv);
// FilledRects do not need to be released.
if (image_id == allocation::kInvalidImageId)
continue;
// Remember all dead images so that we can release them in the destructor if necessary.
// Typically this won't be necessary: we'll release them as soon as it is safe (roughly, when
// the next present takes effect).
images_to_release_->insert(image_id);
images_to_release.push_back(image_id);
}
}
return images_to_release;
}
void Flatland::AddChild(TransformId parent_transform_id, TransformId child_transform_id) {
if (parent_transform_id.value == kInvalidId || child_transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "AddChild called with transform_id zero";
ReportBadOperationError();
return;
}
auto parent_global_kv = transforms_.find(parent_transform_id.value);
auto child_global_kv = transforms_.find(child_transform_id.value);
if (parent_global_kv == transforms_.end()) {
error_reporter_->ERROR() << "AddChild failed, parent_transform_id " << parent_transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
if (child_global_kv == transforms_.end()) {
error_reporter_->ERROR() << "AddChild failed, child_transform_id " << child_transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
bool added = transform_graph_.AddChild(parent_global_kv->second, child_global_kv->second);
if (!added) {
error_reporter_->ERROR() << "AddChild failed, connection already exists between parent "
<< parent_transform_id.value << " and child "
<< child_transform_id.value;
ReportBadOperationError();
}
}
void Flatland::RemoveChild(TransformId parent_transform_id, TransformId child_transform_id) {
if (parent_transform_id.value == kInvalidId || child_transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "RemoveChild failed, transform_id " << parent_transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
auto parent_global_kv = transforms_.find(parent_transform_id.value);
auto child_global_kv = transforms_.find(child_transform_id.value);
if (parent_global_kv == transforms_.end()) {
error_reporter_->ERROR() << "RemoveChild failed, parent_transform_id "
<< parent_transform_id.value << " not found";
ReportBadOperationError();
return;
}
if (child_global_kv == transforms_.end()) {
error_reporter_->ERROR() << "RemoveChild failed, child_transform_id "
<< child_transform_id.value << " not found";
ReportBadOperationError();
return;
}
bool removed = transform_graph_.RemoveChild(parent_global_kv->second, child_global_kv->second);
if (!removed) {
error_reporter_->ERROR() << "RemoveChild failed, connection between parent "
<< parent_transform_id.value << " and child "
<< child_transform_id.value << " not found";
ReportBadOperationError();
}
}
void Flatland::SetRootTransform(TransformId transform_id) {
// SetRootTransform(0) is special -- it only clears the existing root transform.
if (transform_id.value == kInvalidId) {
transform_graph_.ClearChildren(local_root_);
return;
}
const auto global_kv = transforms_.find(transform_id.value);
if (global_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetRootTransform failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
transform_graph_.ClearChildren(local_root_);
bool added = transform_graph_.AddChild(local_root_, global_kv->second);
FX_DCHECK(added);
root_transform_ = global_kv->second;
}
void Flatland::CreateViewport(ContentId link_id, ViewportCreationToken token,
ViewportProperties properties,
fidl::InterfaceRequest<ChildViewWatcher> child_view_watcher) {
TRACE_DURATION("gfx", "Flatland::CreateViewport", "debug_name", TA_STRING(debug_name_.c_str()));
// Attempting to link with an invalid token will never succeed, so its better to fail early and
// immediately close the link connection.
if (!token.value.is_valid()) {
error_reporter_->ERROR() << "CreateViewport failed, ViewportCreationToken was invalid";
ReportBadOperationError();
return;
}
if (!properties.has_logical_size()) {
error_reporter_->ERROR()
<< "CreateViewport must be provided a ViewportProperties with a logical size";
ReportBadOperationError();
return;
}
if (auto error = ValidateViewportProperties(properties)) {
error_reporter_->ERROR() << "CreateViewport failed: " << *error;
ReportBadOperationError();
return;
}
SetViewportPropertiesMissingDefaults(properties, properties.logical_size(),
/*inset*/ {0, 0, 0, 0});
if (link_id.value == kInvalidId) {
error_reporter_->ERROR() << "CreateViewport called with ContentId zero";
ReportBadOperationError();
return;
}
if (content_handles_.count(link_id.value)) {
error_reporter_->ERROR() << "CreateViewport called with existing ContentId " << link_id.value;
ReportBadOperationError();
return;
}
FX_DCHECK(link_system_);
// The ViewportProperties and ChildViewWatcherImpl live on a handle from this Flatland instance.
const auto parent_transform_handle = transform_graph_.CreateTransform();
// We can initialize the Link importer immediately, since no state changes actually occur before
// the feed-forward portion of this method. We also forward the initial ViewportProperties through
// the LinkSystem immediately, so the child can receive them as soon as possible.
LinkSystem::LinkToChild link_to_child = link_system_->CreateLinkToChild(
dispatcher_holder_, std::move(token), fidl::Clone(properties), std::move(child_view_watcher),
parent_transform_handle,
[ref = weak_from_this(), weak_dispatcher_holder = std::weak_ptr<utils::DispatcherHolder>(
dispatcher_holder_)](const std::string& error_log) {
if (auto dispatcher_holder = weak_dispatcher_holder.lock()) {
FX_CHECK(dispatcher_holder->dispatcher() == async_get_default_dispatcher())
<< "Link protocol error reported on the wrong dispatcher.";
}
if (auto impl = ref.lock())
impl->ReportLinkProtocolError(error_log);
});
// This is the feed-forward portion of the method. Here, we add the link to the map, and
// initialize its layout with the desired properties. The Link will not actually result in
// additions to the Transform hierarchy until it is added to a Transform.
{
const bool child_added = transform_graph_.AddChild(link_to_child.parent_transform_handle,
link_to_child.internal_link_handle);
FX_DCHECK(child_added);
}
FLATLAND_VERBOSE_LOG << "Flatland::CreateViewport() in " << local_root_
<< " parent_transform_handle: " << link_to_child.parent_transform_handle
<< " internal_link_handle: " << link_to_child.internal_link_handle;
// Default the link size to the logical size, which is just an identity scale matrix, so
// that future logical size changes will result in the correct scale matrix.
const SizeU size = properties.logical_size();
content_handles_[link_id.value] = link_to_child.parent_transform_handle;
links_to_children_[link_to_child.parent_transform_handle] = {.link = std::move(link_to_child),
.properties = std::move(properties)};
// Set clip bounds on the transform associated with the viewport content.
SetClipBoundaryInternal(parent_transform_handle, {.x = 0,
.y = 0,
.width = static_cast<int32_t>(size.width),
.height = static_cast<int32_t>(size.height)});
}
void Flatland::CreateImage(ContentId image_id,
fuchsia::ui::composition::BufferCollectionImportToken import_token,
uint32_t vmo_index, ImageProperties properties) {
TRACE_DURATION("gfx", "Flatland::CreateImage", "debug_name", TA_STRING(debug_name_.c_str()));
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "CreateImage called with image_id 0";
ReportBadOperationError();
return;
}
if (content_handles_.count(image_id.value)) {
error_reporter_->ERROR() << "CreateImage called with pre-existing image_id " << image_id.value;
ReportBadOperationError();
return;
}
const BufferCollectionId global_collection_id = fsl::GetRelatedKoid(import_token.value.get());
// Check if there is a valid peer.
if (global_collection_id == ZX_KOID_INVALID) {
error_reporter_->ERROR() << "CreateImage called with no valid export token";
ReportBadOperationError();
return;
}
if (!properties.has_size()) {
error_reporter_->ERROR() << "CreateImage failed, ImageProperties did not specify size";
ReportBadOperationError();
return;
}
if (!properties.size().width) {
error_reporter_->ERROR() << "CreateImage failed, ImageProperties did not specify a width";
ReportBadOperationError();
return;
}
if (!properties.size().height) {
error_reporter_->ERROR() << "CreateImage failed, ImageProperties did not specify a height";
ReportBadOperationError();
return;
}
allocation::ImageMetadata metadata;
metadata.identifier = allocation::GenerateUniqueImageId();
metadata.collection_id = global_collection_id;
metadata.vmo_index = vmo_index;
metadata.width = properties.size().width;
metadata.height = properties.size().height;
metadata.blend_mode = fuchsia::ui::composition::BlendMode::SRC;
for (uint32_t i = 0; i < buffer_collection_importers_.size(); i++) {
auto& importer = buffer_collection_importers_[i];
// TODO(https://fxbug.dev/42140615): Give more detailed errors.
auto result =
importer->ImportBufferImage(metadata, allocation::BufferCollectionUsage::kClientImage);
if (!result) {
// If this importer fails, we need to release the image from
// all of the importers that it passed on. Luckily we can do
// this right here instead of waiting for a fence since we know
// this image isn't being used by anything yet.
for (uint32_t j = 0; j < i; j++) {
buffer_collection_importers_[j]->ReleaseBufferImage(metadata.identifier);
}
error_reporter_->ERROR() << "Importer could not import image.";
ReportBadOperationError();
return;
}
}
// Now that we've successfully been able to import the image into the importers,
// we can now create a handle for it in the transform graph, and add the metadata
// to our map.
auto handle = transform_graph_.CreateTransform();
content_handles_[image_id.value] = handle;
image_metadatas_[handle] = metadata;
// Set the default sample region of the image to be the full image.
SetImageSampleRegion(image_id, {0, 0, static_cast<float>(properties.size().width),
static_cast<float>(properties.size().height)});
// Set the default destination region of the image to be the full image.
SetImageDestinationSize(image_id, properties.size());
FLATLAND_VERBOSE_LOG << "Flatland::CreateImage" << handle << " for " << local_root_
<< " size:" << properties.size().width << "x" << properties.size().height;
}
void Flatland::SetImageSampleRegion(ContentId image_id, RectF rect) {
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetImageSampleRegion called with content id 0";
ReportBadOperationError();
return;
}
const auto content_kv = content_handles_.find(image_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetImageSampleRegion called with non-existent image_id "
<< image_id.value;
ReportBadOperationError();
return;
}
const auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "SetImageSampleRegion called on non-image content.";
ReportBadOperationError();
return;
}
// The provided sample region needs to be within the bounds of the image.
{
const auto& metadata = image_kv->second;
const auto image_width = static_cast<float>(metadata.width);
const auto image_height = static_cast<float>(metadata.height);
// This clamping is required in cases where (x+width>image_width) or (y+height>image_height) by
// a small epsilon. The downstream code expects these numbers to be within the (image_width,
// image_height) limits, so we only clamp the positive differences. The root cause is the
// precision errors in floating point arithmetic when a client tries to calculate floats within
// pixel space.
// TODO(https://fxbug.dev/42082599): Remove floating point precision error checks and use uints
// instead.
ClampIfNear(&rect.width, rect.x + rect.width - image_width);
ClampIfNear(&rect.height, rect.y + rect.height - image_height);
if (rect.x < 0.f || rect.width < 0.f || (rect.x + rect.width) > image_width || rect.y < 0.f ||
rect.height < 0.f || (rect.y + rect.height) > image_height) {
error_reporter_->ERROR() << "SetImageSampleRegion rect " << rect
<< " out of bounds for image (" << image_width << ", "
<< image_height << ")";
ReportBadOperationError();
return;
}
}
image_sample_regions_[content_kv->second] = rect;
}
void Flatland::SetImageDestinationSize(ContentId image_id, SizeU size) {
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetImageSize called with image_id 0";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(image_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetImageSize called with non-existent image_id " << image_id.value;
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "SetImageSize called on non-image content " << image_id.value;
ReportBadOperationError();
return;
}
matrices_[content_kv->second].SetScale(
{.x = static_cast<float>(size.width), .y = static_cast<float>(size.height)});
}
void Flatland::SetImageBlendingFunction(ContentId image_id,
fuchsia::ui::composition::BlendMode blend_mode) {
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetImageBlendingFunction called with content id 0";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(image_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetImageBlendingFunction called with non-existent image_id "
<< image_id.value;
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "SetImageBlendingFunction called on non-image content.";
ReportBadOperationError();
return;
}
image_kv->second.blend_mode = blend_mode;
}
void Flatland::SetImageFlip(ContentId image_id, fuchsia::ui::composition::ImageFlip flip) {
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetImageBlendingFunction called with content id 0";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(image_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetImageBlendingFunction called with non-existent image_id "
<< image_id.value;
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "SetImageFlip called on non-image content.";
ReportBadOperationError();
return;
}
image_kv->second.flip = flip;
}
void Flatland::CreateFilledRect(ContentId rect_id) {
if (rect_id.value == kInvalidId) {
error_reporter_->ERROR() << "CreateFilledRect called with rect_id 0";
ReportBadOperationError();
return;
}
if (content_handles_.count(rect_id.value)) {
error_reporter_->ERROR() << "CreateFilledRect called with pre-existing content id "
<< rect_id.value;
ReportBadOperationError();
return;
}
allocation::ImageMetadata metadata;
// allocation::kInvalidImageId is overloaded in the renderer to signal that a
// default 1x1 white texture should be applied to this rectangle.
metadata.identifier = allocation::kInvalidImageId;
metadata.blend_mode = fuchsia::ui::composition::BlendMode::SRC;
// Now that we've successfully been able to import the image into the importers,
// we can now create a handle for it in the transform graph, and add the metadata
// to our map.
auto handle = transform_graph_.CreateTransform();
content_handles_[rect_id.value] = handle;
image_metadatas_[handle] = metadata;
}
void Flatland::SetSolidFill(ContentId rect_id, fuchsia::ui::composition::ColorRgba color,
fuchsia::math::SizeU size) {
if (rect_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetSolidFill called with rect_id 0";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(rect_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetSolidFill called with non-existent rect_id " << rect_id.value;
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "Missing metadada for rect with id " << rect_id.value;
ReportBadOperationError();
return;
}
if (color.red < 0.f || color.red > 1.f || isinf(color.red) || isnan(color.red) ||
color.green < 0.f || color.green > 1.f || isinf(color.green) || isnan(color.green) ||
color.blue < 0.f || color.blue > 1.f || isinf(color.blue) || isnan(color.blue) ||
color.alpha < 0.f || color.alpha > 1.f || isinf(color.alpha) || isnan(color.alpha)) {
error_reporter_->ERROR() << "Invalid color channel(s) (" << color.red << ", " << color.green
<< ", " << color.blue << ", " << color.alpha << ")";
ReportBadOperationError();
return;
}
image_kv->second.blend_mode = color.alpha < 1.f ? fuchsia::ui::composition::BlendMode::SRC_OVER
: fuchsia::ui::composition::BlendMode::SRC;
image_kv->second.collection_id = allocation::kInvalidId;
image_kv->second.identifier = allocation::kInvalidImageId;
image_kv->second.multiply_color = {color.red, color.green, color.blue, color.alpha};
matrices_[content_kv->second].SetScale(
{.x = static_cast<float>(size.width), .y = static_cast<float>(size.height)});
}
void Flatland::ReleaseFilledRect(ContentId rect_id) {
if (rect_id.value == kInvalidId) {
error_reporter_->ERROR() << "ReleaseFilledRect called with rect_id zero";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(rect_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "ReleaseFilledRect failed, rect_id " << rect_id.value
<< " not found";
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "ReleaseFilledRect failed, content_id " << rect_id.value
<< " has no metadata.";
ReportBadOperationError();
return;
}
bool erased_from_graph = transform_graph_.ReleaseTransform(content_kv->second);
FX_DCHECK(erased_from_graph);
// Even though the handle is released, it may still be referenced by client Transforms. The
// image_metadatas_ map preserves the entry until it shows up in the dead_transforms list.
content_handles_.erase(rect_id.value);
}
void Flatland::SetImageOpacity(ContentId image_id, float val) {
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetImageOpacity called with invalid image_id";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(image_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetImageOpacity called with non-existent image_id "
<< image_id.value;
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "SetImageOpacity called on non-rectangle content.";
ReportBadOperationError();
return;
}
auto& metadata = image_kv->second;
if (metadata.identifier == allocation::kInvalidImageId) {
error_reporter_->ERROR() << "SetImageOpacity called on solid color content.";
ReportBadOperationError();
return;
}
if (val < 0.f || val > 1.f) {
error_reporter_->ERROR() << "Opacity value is not within valid range [0,1].";
ReportBadOperationError();
return;
}
// Opacity is stored as the alpha channel of the multiply color.
metadata.multiply_color[3] = val;
}
void Flatland::SetHitRegions(TransformId transform_id,
std::vector<fuchsia::ui::composition::HitRegion> regions) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetHitRegions called with invalid transform ID";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetHitRegions failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
// Validate |regions|.
for (auto& region : regions) {
auto rect = region.region;
if (rect.width < 0 || rect.height < 0) {
error_reporter_->ERROR() << "SetHitRegions failed, contains invalid (negative) dimensions: ("
<< rect.width << "," << rect.height << ")";
ReportBadOperationError();
return;
}
}
// Reformat into internal type.
std::vector<flatland::HitRegion> list;
for (auto& region : regions) {
list.emplace_back(region.region, region.hit_test);
}
hit_regions_[transform_kv->second] = list;
}
void Flatland::SetInfiniteHitRegion(TransformId transform_id,
fuchsia::ui::composition::HitTestInteraction hit_test) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetHitRegions called with invalid transform ID";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetHitRegions failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
hit_regions_[transform_kv->second] = {flatland::HitRegion::Infinite(hit_test)};
}
void Flatland::SetContent(TransformId transform_id, ContentId content_id) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetContent called with transform_id zero";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "SetContent failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
if (content_id.value == kInvalidId) {
transform_graph_.ClearPriorityChild(transform_kv->second);
FLATLAND_VERBOSE_LOG << "Flatland::SetContent() cleared content for transform: "
<< transform_kv->second;
return;
}
auto handle_kv = content_handles_.find(content_id.value);
if (handle_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetContent failed, content_id " << content_id.value
<< " not found";
ReportBadOperationError();
return;
}
FLATLAND_VERBOSE_LOG << "Flatland::SetContent(" << transform_kv->second << ","
<< handle_kv->second << ")";
transform_graph_.SetPriorityChild(transform_kv->second, handle_kv->second);
}
void Flatland::SetViewportProperties(ContentId link_id, ViewportProperties properties) {
if (link_id.value == kInvalidId) {
error_reporter_->ERROR() << "SetViewportProperties called with link_id zero.";
ReportBadOperationError();
return;
}
const auto content_kv = content_handles_.find(link_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "SetViewportProperties failed, link_id " << link_id.value
<< " not found";
ReportBadOperationError();
return;
}
const auto viewport_handle = content_kv->second;
auto link_kv = links_to_children_.find(viewport_handle);
if (link_kv == links_to_children_.end()) {
error_reporter_->ERROR() << "SetViewportProperties failed, content_id " << link_id.value
<< " is not a Link";
ReportBadOperationError();
return;
}
LinkToChildData& link_data = link_kv->second;
if (!link_data.link.importer.valid()) {
// Other side of the Viewport has been invalidated and the Viewport should be released. Calling
// SetViewportProperties() must still be allowed since the client may not have gotten the
// destruction message yet.
return;
}
if (auto error = ValidateViewportProperties(properties)) {
error_reporter_->ERROR() << "SetViewportProperties failed: " << *error;
ReportBadOperationError();
return;
}
SetViewportPropertiesMissingDefaults(properties, link_data.properties.logical_size(),
link_data.properties.inset());
// Update the clip boundaries when the properties change.
SetClipBoundaryInternal(viewport_handle,
{.x = 0,
.y = 0,
.width = static_cast<int32_t>(properties.logical_size().width),
.height = static_cast<int32_t>(properties.logical_size().height)});
link_data.properties = fidl::Clone(properties);
link_system_->UpdateViewportPropertiesFor(viewport_handle, std::move(properties));
}
void Flatland::ReleaseTransform(TransformId transform_id) {
if (transform_id.value == kInvalidId) {
error_reporter_->ERROR() << "ReleaseTransform called with transform_id zero";
ReportBadOperationError();
return;
}
auto transform_kv = transforms_.find(transform_id.value);
if (transform_kv == transforms_.end()) {
error_reporter_->ERROR() << "ReleaseTransform failed, transform_id " << transform_id.value
<< " not found";
ReportBadOperationError();
return;
}
bool erased_from_graph = transform_graph_.ReleaseTransform(transform_kv->second);
FX_DCHECK(erased_from_graph);
transforms_.erase(transform_kv);
}
void Flatland::ReleaseViewport(
ContentId link_id, fuchsia::ui::composition::Flatland::ReleaseViewportCallback callback) {
if (link_id.value == kInvalidId) {
error_reporter_->ERROR() << "ReleaseViewport called with link_id zero";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(link_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "ReleaseViewport failed, link_id " << link_id.value << " not found";
ReportBadOperationError();
return;
}
auto link_kv = links_to_children_.find(content_kv->second);
if (link_kv == links_to_children_.end()) {
error_reporter_->ERROR() << "ReleaseViewport failed, content_id " << link_id.value
<< " is not a Link";
ReportBadOperationError();
return;
}
LinkToChildData& link_data = link_kv->second;
// Deleting the LinkToChild's |parent_transform_handle| effectively deletes the link from
// the local topology, even if the link object itself is not deleted.
{
const bool child_removed = transform_graph_.RemoveChild(link_data.link.parent_transform_handle,
link_data.link.internal_link_handle);
FX_DCHECK(child_removed);
const bool content_released =
transform_graph_.ReleaseTransform(link_data.link.parent_transform_handle);
FX_DCHECK(content_released);
}
// Move the old child link into the delayed operation so that the ContentId is immediately free
// for re-use, but it doesn't get deleted until after the new UberStruct is published.
auto link_to_child = std::move(link_data);
links_to_children_.erase(content_kv->second);
content_handles_.erase(content_kv);
// Delay the actual destruction of the link until the next Present().
pending_link_operations_.push_back(
[link_to_child = std::move(link_to_child), callback = std::move(callback)]() mutable {
ViewportCreationToken return_token;
// If the link is still valid, return the original token. If not, create an orphaned
// zx::channel and return it since the ObjectLinker does not retain the orphaned token.
auto link_token = link_to_child.link.importer.ReleaseToken();
if (link_token.has_value()) {
return_token.value = zx::channel(std::move(link_token.value()));
} else {
// |peer_token| immediately falls out of scope, orphaning |return_token|.
zx::channel peer_token;
zx::channel::create(0, &return_token.value, &peer_token);
}
callback(std::move(return_token));
});
}
void Flatland::ReleaseImage(ContentId image_id) {
if (image_id.value == kInvalidId) {
error_reporter_->ERROR() << "ReleaseImage called with image_id zero";
ReportBadOperationError();
return;
}
auto content_kv = content_handles_.find(image_id.value);
if (content_kv == content_handles_.end()) {
error_reporter_->ERROR() << "ReleaseImage failed, image_id " << image_id.value << " not found";
ReportBadOperationError();
return;
}
auto image_kv = image_metadatas_.find(content_kv->second);
if (image_kv == image_metadatas_.end()) {
error_reporter_->ERROR() << "ReleaseImage failed, content_id " << image_id.value
<< " is not an Image";
ReportBadOperationError();
return;
}
FLATLAND_VERBOSE_LOG << "Flatland::ReleaseImage" << content_kv->second << " for " << local_root_;
bool erased_from_graph = transform_graph_.ReleaseTransform(content_kv->second);
FX_DCHECK(erased_from_graph);
// Even though the handle is released, it may still be referenced by client Transforms. The
// image_metadatas_ map preserves the entry until it shows up in the dead_transforms list.
content_handles_.erase(image_id.value);
}
void Flatland::SetDebugName(std::string name) {
TRACE_INSTANT("gfx", "Flatland::SetDebugName()", TRACE_SCOPE_PROCESS, "name",
TA_STRING(name.c_str()));
std::stringstream stream;
if (!name.empty())
stream << "Flatland client(" << name << "): ";
FLATLAND_VERBOSE_LOG << "Flatland::SetDebugName() to " << stream.str() << " for " << local_root_
<< " " << this;
error_reporter_->SetPrefix(stream.str());
debug_name_ = std::move(name);
}
void Flatland::OnNextFrameBegin(uint32_t additional_present_credits,
FuturePresentationInfos presentation_infos) {
TRACE_DURATION("gfx", "Flatland::OnNextFrameBegin");
present_credits_ += additional_present_credits;
// Only send an `OnNextFrameBegin` event if the client has at least one present credit. It is
// guaranteed that this won't stall clients because the current policy is to always return
// present tokens upon processing them. If and when a new policy is adopted, we should take care
// to ensure this guarantee is upheld.
if (present_credits_ > 0 && binding_.is_bound()) {
OnNextFrameBeginValues values;
values.set_additional_present_credits(additional_present_credits);
values.set_future_presentation_infos(std::move(presentation_infos));
binding_.events().OnNextFrameBegin(std::move(values));
}
}
void Flatland::OnFramePresented(const std::map<scheduling::PresentId, zx::time>& latched_times,
scheduling::PresentTimestamps present_times) {
TRACE_DURATION("gfx", "Flatland::OnFramePresented");
// TODO(https://fxbug.dev/42141795): remove `num_presents_allowed` from this event. Clients
// should obtain this information from OnPresentProcessedValues().
present2_helper_.OnPresented(latched_times, present_times, /*num_presents_allowed=*/0);
}
TransformHandle Flatland::GetRoot() const {
return link_to_parent_ ? link_to_parent_->child_transform_handle : local_root_;
}
std::optional<TransformHandle> Flatland::GetContentHandle(ContentId content_id) const {
auto handle_kv = content_handles_.find(content_id.value);
if (handle_kv == content_handles_.end()) {
return std::nullopt;
}
return handle_kv->second;
}
// For validating properties associated with transforms in tests only. If |transform_id| does not
// exist for this Flatland instance, returns std::nullopt.
std::optional<TransformHandle> Flatland::GetTransformHandle(TransformId transform_id) const {
auto handle_kv = transforms_.find(transform_id.value);
if (handle_kv == transforms_.end()) {
return std::nullopt;
}
return handle_kv->second;
}
void Flatland::SetErrorReporter(std::unique_ptr<scenic_impl::ErrorReporter> error_reporter) {
error_reporter_ = std::move(error_reporter);
}
scheduling::SessionId Flatland::GetSessionId() const { return session_id_; }
void Flatland::ReportBadOperationError() { failure_since_previous_present_ = true; }
void Flatland::ReportLinkProtocolError(const std::string& error_log) {
error_reporter_->ERROR() << error_log;
link_protocol_error_ = true;
}
void Flatland::CloseConnection(FlatlandError error) {
// NOTE: there's no need to test the return values of OnError()/Cancel()/Close(). If they fail,
// the binding and waiter will be cleaned up anyway because we'll soon be destroyed (since
// destroy_instance_function_ has been or will be invoked).
// Send the error to the client before closing the connection.
binding_.events().OnError(error);
// Cancel the async::Wait before closing the connection, or it will assert on destruction.
peer_closed_waiter_.Cancel();
// Immediately close the FIDL interface to prevent future requests.
binding_.Close(ZX_ERR_BAD_STATE);
// Finally, trigger the destruction of this instance.
//
// NOTE: it would probably be OK to test |destroy_instance_function_was_invoked_| at the top of
// the function, exiting early if it was already invoked. But this way makes it obvious that the
// cleanups above run at least once (and there's no downside if they are run a second time).
if (!destroy_instance_function_was_invoked_) {
destroy_instance_function_was_invoked_ = true;
destroy_instance_function_();
}
}
// MatrixData function implementations
// static
float Flatland::MatrixData::GetOrientationAngle(fuchsia::ui::composition::Orientation orientation) {
// The matrix is specified in view-space coordinates, in which the +y axis points downwards (not
// upwards). Rotations which are specified as counter-clockwise must actually occur in a clockwise
// fashion in this coordinate space (a vector on the +x axis rotates towards -y axis to give the
// appearance of a counter-clockwise rotation).
switch (orientation) {
case Orientation::CCW_0_DEGREES:
return 0.f;
case Orientation::CCW_90_DEGREES:
return -glm::half_pi<float>();
case Orientation::CCW_180_DEGREES:
return -glm::pi<float>();
case Orientation::CCW_270_DEGREES:
return -glm::three_over_two_pi<float>();
}
}
void Flatland::MatrixData::SetTranslation(Vec translation) {
translation_.x = static_cast<float>(translation.x);
translation_.y = static_cast<float>(translation.y);
RecomputeMatrix();
}
void Flatland::MatrixData::SetOrientation(fuchsia::ui::composition::Orientation orientation) {
angle_ = GetOrientationAngle(orientation);
RecomputeMatrix();
}
void Flatland::MatrixData::SetScale(VecF scale) {
scale_.x = scale.x;
scale_.y = scale.y;
RecomputeMatrix();
}
void Flatland::MatrixData::RecomputeMatrix() {
// Manually compose the matrix rather than use glm transformations since the order of operations
// is always the same. glm matrices are column-major.
float* vals = static_cast<float*>(glm::value_ptr(matrix_));
// Translation in the third column.
vals[6] = translation_.x;
vals[7] = translation_.y;
// Rotation and scale combined into the first two columns.
const float s = sin(angle_);
const float c = cos(angle_);
vals[0] = c * scale_.x;
vals[1] = s * scale_.x;
vals[3] = -1.f * s * scale_.y;
vals[4] = c * scale_.y;
}
glm::mat3 Flatland::MatrixData::GetMatrix() const { return matrix_; }
} // namespace flatland