blob: abf7fe9869b76feaa63c3b32acd71a8b67d3e936 [file] [log] [blame] [edit]
// 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 <fidl/fuchsia.hardware.display.types/cpp/fidl.h>
#include <fidl/fuchsia.ui.composition/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/time.h>
#include <lib/sync/cpp/completion.h>
#include <lib/sys/cpp/testing/component_context_provider.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/ui/scenic/cpp/buffer_collection_import_export_tokens.h>
#include <lib/ui/scenic/cpp/view_creation_tokens.h>
#include <lib/ui/scenic/cpp/view_identity.h>
#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <gtest/gtest.h>
#include "fuchsia/ui/composition/cpp/fidl.h"
#include "lib/async/default.h"
#include "lib/fidl/cpp/hlcpp_conversion.h"
#include "src/lib/fsl/handles/object_info.h"
#include "src/ui/lib/escher/util/epsilon_compare.h"
#include "src/ui/scenic/lib/allocation/allocator.h"
#include "src/ui/scenic/lib/allocation/buffer_collection_importer.h"
#include "src/ui/scenic/lib/allocation/id.h"
#include "src/ui/scenic/lib/allocation/mock_buffer_collection_importer.h"
#include "src/ui/scenic/lib/flatland/flatland_display.h"
#include "src/ui/scenic/lib/flatland/flatland_types.h"
#include "src/ui/scenic/lib/flatland/global_matrix_data.h"
#include "src/ui/scenic/lib/flatland/global_topology_data.h"
#include "src/ui/scenic/lib/flatland/tests/logging_event_loop.h"
#include "src/ui/scenic/lib/flatland/tests/mock_flatland_presenter.h"
#include "src/ui/scenic/lib/flatland/uber_struct_system.h"
#include "src/ui/scenic/lib/scenic/util/error_reporter.h"
#include "src/ui/scenic/lib/scheduling/frame_scheduler.h"
#include "src/ui/scenic/lib/scheduling/id.h"
#include "src/ui/scenic/lib/utils/dispatcher_holder.h"
#include "src/ui/scenic/lib/utils/helpers.h"
#include "zircon/errors.h"
#include <glm/gtx/matrix_transform_2d.hpp>
using ::testing::_;
using ::testing::Return;
using BufferCollectionId = flatland::Flatland::BufferCollectionId;
using allocation::Allocator;
using allocation::BufferCollectionImporter;
using allocation::ImageMetadata;
using allocation::MockBufferCollectionImporter;
using allocation::cpp::BufferCollectionImportExportTokens;
using flatland::Flatland;
using flatland::FlatlandDisplay;
using flatland::FlatlandPresenter;
using flatland::GlobalMatrixVector;
using flatland::GlobalTopologyData;
using flatland::LinkSystem;
using flatland::MockFlatlandPresenter;
using flatland::TransformGraph;
using flatland::TransformHandle;
using flatland::UberStruct;
using flatland::UberStructSystem;
using fuchsia_math::SizeU;
using fuchsia_ui_composition::BufferCollectionExportToken;
using fuchsia_ui_composition::BufferCollectionImportToken;
using fuchsia_ui_composition::ChildViewStatus;
using fuchsia_ui_composition::ChildViewWatcher;
using fuchsia_ui_composition::ContentId;
using fuchsia_ui_composition::FlatlandError;
using fuchsia_ui_composition::ImageProperties;
using fuchsia_ui_composition::LayoutInfo;
using fuchsia_ui_composition::OnNextFrameBeginValues;
using fuchsia_ui_composition::Orientation;
using fuchsia_ui_composition::ParentViewportStatus;
using fuchsia_ui_composition::ParentViewportWatcher;
using fuchsia_ui_composition::TransformId;
using fuchsia_ui_composition::ViewportProperties;
using fuchsia_ui_views::ViewCreationToken;
using fuchsia_ui_views::ViewportCreationToken;
using ChildViewWatcher_GetStatusResult =
fidl::Result<fuchsia_ui_composition::ChildViewWatcher::GetStatus>;
using ChildViewWatcher_GetViewRefResult =
fidl::Result<fuchsia_ui_composition::ChildViewWatcher::GetViewRef>;
using ParentViewportWatcher_GetLayoutResult =
fidl::Result<fuchsia_ui_composition::ParentViewportWatcher::GetLayout>;
using ParentViewportWatcher_GetStatusResult =
fidl::Result<fuchsia_ui_composition::ParentViewportWatcher::GetStatus>;
namespace {
// Convenience struct for the PRESENT_WITH_ARGS macro to avoid having to update it every time
// a new argument is added to Flatland::Present(). This struct also includes additional flags
// to PRESENT_WITH_ARGS itself for testing timing-related Present() functionality.
struct PresentArgs {
// Arguments to Flatland::Present().
zx::time requested_presentation_time;
std::vector<zx::event> acquire_fences;
std::vector<zx::event> release_fences;
bool unsquashable = false;
// Arguments to the PRESENT_WITH_ARGS macro.
// If true, skips the session update associated with the Present(), meaning the new UberStruct
// will not be in the snapshot and the release fences will not be signaled.
bool skip_session_update_and_release_fences = false;
// The number of present tokens that should be returned to the client.
uint32_t present_credits_returned = 1;
// The future presentation infos that should be returned to the client.
flatland::Flatland::FuturePresentationInfos presentation_infos = {};
// If PRESENT_WITH_ARGS is called with |expect_success| = false, the error that should be
// expected as the return value from Present().
FlatlandError expected_error = FlatlandError::kBadOperation;
};
struct GlobalIdPair {
allocation::GlobalBufferCollectionId collection_id;
allocation::GlobalImageId image_id;
};
// These macros works like functions that check a variety of conditions, but if those conditions
// fail, the line number for the failure will appear in-line rather than in a function.
// This macro calls Present() on a Flatland object and immediately triggers the session update
// for all sessions so that changes from that Present() are visible in global systems. This is
// primarily useful for testing the user-facing Flatland API.
//
// This macro must be used within a test using the FlatlandTest harness.
//
// |flatland| is a Flatland object constructed with the MockFlatlandPresenter owned by the
// FlatlandTest harness. |expect_success| should be false if the call to Present() is expected to
// trigger an error.
#define PRESENT_WITH_ARGS(flatland, args, expect_success) \
{ \
bool had_acquire_fences = !(args).acquire_fences.empty(); \
bool processed_callback = false; \
fuchsia_ui_composition::PresentArgs present_args; \
present_args.requested_presentation_time((args).requested_presentation_time.get()) \
.acquire_fences(std::move((args).acquire_fences)) \
.release_fences(std::move((args).release_fences)) \
.unsquashable((args).unsquashable); \
(flatland)->Present(std::move(present_args)); \
if (expect_success) { \
/* Even with no acquire_fences, UberStruct updates queue on the dispatcher. */ \
if (!had_acquire_fences) { \
EXPECT_CALL(*mock_flatland_presenter_, \
ScheduleUpdateForSession((args).requested_presentation_time, _, \
(args).unsquashable, _, _)); \
} \
RunLoopUntilIdle(); \
if (!(args).skip_session_update_and_release_fences) { \
ApplySessionUpdatesAndSignalFences(); \
} \
(flatland)->OnNextFrameBegin((args).present_credits_returned, \
std::move((args).presentation_infos)); \
} else { \
RunLoopUntilIdle(); \
EXPECT_EQ(GetPresentError((flatland)->GetSessionId()), (args).expected_error); \
} \
}
// Identical to PRESENT_WITH_ARGS, but supplies an empty PresentArgs to the Present() call.
#define PRESENT(flatland, expect_success) \
{ \
PRESENT_WITH_ARGS(flatland, PresentArgs(), expect_success); \
}
#define REGISTER_BUFFER_COLLECTION(allocator, bc_export_token, token, expect_success) \
if (expect_success) { \
EXPECT_CALL(*mock_buffer_collection_importer_, \
ImportBufferCollection(fsl::GetKoid(bc_export_token.value().get()), _, _, _, _)) \
.WillOnce(testing::Invoke( \
[](allocation::GlobalBufferCollectionId, fuchsia::sysmem2::Allocator_Sync*, \
fidl::InterfaceHandle<fuchsia::sysmem2::BufferCollectionToken>, \
allocation::BufferCollectionUsage, \
std::optional<fuchsia::math::SizeU>) { return true; })); \
} \
bool processed_callback = false; \
fuchsia_ui_composition::RegisterBufferCollectionArgs args; \
args.export_token(std::move(bc_export_token)); \
args.buffer_collection_token2( \
fidl::ClientEnd<fuchsia_sysmem2::BufferCollectionToken>(std::move(token).TakeChannel())); \
allocator->RegisterBufferCollection(std::move(args), [&processed_callback](auto result) { \
EXPECT_EQ(expect_success, result.is_ok()); \
processed_callback = true; \
}); \
EXPECT_TRUE(processed_callback);
// This macro searches for a local matrix associated with a specific TransformHandle.
//
// |uber_struct| is the UberStruct to search to find the matrix. |target_handle| is the
// TransformHandle of the matrix to compare. |expected_matrix| is the expected value of that
// matrix.
#define EXPECT_MATRIX(uber_struct, target_handle, expected_matrix) \
{ \
glm::mat3 matrix = glm::mat3(); \
auto matrix_kv = uber_struct->local_matrices.find(target_handle); \
if (matrix_kv != uber_struct->local_matrices.end()) { \
matrix = matrix_kv->second; \
} \
for (size_t i = 0; i < 3; ++i) { \
for (size_t j = 0; j < 3; ++j) { \
EXPECT_FLOAT_EQ(matrix[i][j], expected_matrix[i][j]) << " row " << j << " column " << i; \
} \
} \
}
const uint32_t kDefaultSize = 1;
const glm::vec2 kDefaultDisplayPixelRatio = {1.0f, 1.0f};
const int32_t kDefaultInset = 0;
fuchsia_ui_composition::ViewBoundProtocols NoViewProtocols() { return {}; }
// Testing FlatlandDisplay requires much of the same setup as testing Flatland, so we use the same
// test fixture class (defined immediately below), but renamed to group FlatlandDisplay tests.
class FlatlandTest;
using FlatlandDisplayTest = FlatlandTest;
class EventHandler : public fidl::AsyncEventHandler<fuchsia_ui_composition::Flatland> {
public:
EventHandler(scheduling::SessionId session_id,
std::unordered_map<scheduling::SessionId, FlatlandError>& flatland_errors)
: session_id_(session_id), flatland_errors_(flatland_errors) {}
// Handler for |OnDrawn| events sent from the server.
void OnError(fidl::Event<fuchsia_ui_composition::Flatland::OnError>& event) override {
flatland_errors_[session_id_] = event.error();
}
// Notification that server end of channel was closed.
void on_fidl_error(fidl::UnbindInfo unbind_info) override {
FX_LOGS(INFO) << "Flatland test EventHandler unbound: " << unbind_info;
}
private:
scheduling::SessionId session_id_;
std::unordered_map<scheduling::SessionId, FlatlandError>& flatland_errors_;
};
class FlatlandTest : public LoggingEventLoop, public ::testing::Test {
public:
FlatlandTest()
: uber_struct_system_(std::make_shared<UberStructSystem>()),
link_system_(std::make_shared<LinkSystem>(uber_struct_system_->GetNextInstanceId())) {}
void SetUp() override {
mock_flatland_presenter_ = new ::testing::StrictMock<MockFlatlandPresenter>();
ON_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _))
.WillByDefault(::testing::Invoke(
[&](zx::time requested_presentation_time, scheduling::SchedulingIdPair id_pair,
bool unsquashable, std::vector<zx::event> release_fences, bool schedule_asap) {
// The ID must not already be registered.
EXPECT_FALSE(pending_release_fences_.find(id_pair) != pending_release_fences_.end());
pending_release_fences_[id_pair] = std::move(release_fences);
// Ensure IDs are strictly increasing.
auto current_id_kv = pending_instance_updates_.find(id_pair.session_id);
EXPECT_TRUE(current_id_kv == pending_instance_updates_.end() ||
current_id_kv->second < id_pair.present_id);
// Only save the latest PresentId: the UberStructSystem will flush all Presents prior
// to it.
pending_instance_updates_[id_pair.session_id] = id_pair.present_id;
// Store all requested presentation times to verify in test.
requested_presentation_times_[id_pair] = requested_presentation_time;
}));
ON_CALL(*mock_flatland_presenter_, RemoveSession(_, _))
.WillByDefault(::testing::Invoke(
[](scheduling::SessionId session_id, std::optional<zx::event> release_fence) {
if (release_fence) {
// Pretend that another frame was rendered, causing the release fence to be
// signaled.
release_fence.value().signal(0, ZX_EVENT_SIGNALED);
}
}));
sysmem_allocator_ = utils::CreateSysmemAllocatorSyncPtr("FlatlandTest::SetUp");
flatland_presenter_ = std::shared_ptr<FlatlandPresenter>(mock_flatland_presenter_);
mock_buffer_collection_importer_ = new MockBufferCollectionImporter();
buffer_collection_importer_ =
std::shared_ptr<allocation::BufferCollectionImporter>(mock_buffer_collection_importer_);
// Capture uninteresting cleanup calls from Allocator dtor.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(_, _))
.Times(::testing::AtLeast(0));
// ~Flatland() ensures that RemoveSession will always be called; this is uninteresting.
EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(_, _)).Times(::testing::AtLeast(0));
}
void TearDown() override {
RunLoopUntilIdle();
auto link_topologies = link_system_->GetResolvedTopologyLinks();
EXPECT_TRUE(link_topologies.empty());
buffer_collection_importer_.reset();
flatland_presenter_.reset();
flatlands_.clear();
flatland_displays_.clear();
}
std::shared_ptr<Allocator> CreateAllocator() {
std::vector<std::shared_ptr<BufferCollectionImporter>> importers;
std::vector<std::shared_ptr<BufferCollectionImporter>> screenshot_importers;
importers.push_back(buffer_collection_importer_);
return std::make_shared<Allocator>(
context_provider_.context(), importers, screenshot_importers,
utils::CreateSysmemAllocatorSyncPtr("FlatlandTest::CreateAllocator"));
}
std::shared_ptr<Flatland> CreateFlatland(
fuchsia_ui_composition::TrustedFlatlandConfig config = {}) {
auto session_id = scheduling::GetNextSessionId();
std::vector<std::shared_ptr<BufferCollectionImporter>> importers;
importers.push_back(buffer_collection_importer_);
auto [client_end, server_end] = fidl::Endpoints<fuchsia_ui_composition::Flatland>::Create();
std::shared_ptr<Flatland> flatland = Flatland::New(
std::make_shared<utils::UnownedDispatcherHolder>(this->dispatcher()), std::move(server_end),
session_id,
/*destroy_instance_functon=*/[this, session_id]() { flatland_errors_.erase(session_id); },
flatland_presenter_, link_system_, uber_struct_system_->AllocateQueueForSession(session_id),
importers, [](auto...) {}, [](auto...) {}, [](auto...) {}, [](auto...) {},
std::move(config));
// Wait for server channel to be bound; see `Flatland::Bind()`.
RunLoopUntilIdle();
auto event_handler = std::make_unique<EventHandler>(session_id, flatland_errors_);
fidl::Client client(std::move(client_end), dispatcher(), event_handler.get());
flatlands_.push_back({std::move(client), std::move(event_handler)});
return flatland;
}
// Utility for setting up a client and a server on their own async loops, connected via a FIDL
// channel.
class FlatlandEventLoopClientServer {
public:
using ClientType = fidl::Client<fuchsia_ui_composition::Flatland>;
FlatlandEventLoopClientServer(std::shared_ptr<FlatlandPresenter> presenter,
std::shared_ptr<LinkSystem> link_system,
std::shared_ptr<UberStructSystem> uber_struct_system)
: server_loop_(&kAsyncLoopConfigNoAttachToCurrentThread),
client_loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {
server_loop_.StartThread("flatland-server-loop");
client_loop_.StartThread("flatland-client-loop");
auto session_id = scheduling::GetNextSessionId();
auto flatland_endpoints = fidl::Endpoints<fuchsia_ui_composition::Flatland>::Create();
server_ = Flatland::New(
std::make_shared<utils::UnownedDispatcherHolder>(server_loop_.dispatcher()),
std::move(flatland_endpoints.server), session_id,
/*destroy_instance_function=*/[]() {}, std::move(presenter), std::move(link_system),
uber_struct_system->AllocateQueueForSession(session_id),
/*buffer_collection_importers=*/{}, [](auto...) {}, [](auto...) {}, [](auto...) {},
[](auto...) {}, fuchsia_ui_composition::TrustedFlatlandConfig());
libsync::Completion completion;
async::PostTask(client_loop_.dispatcher(), [&, dispatcher = client_loop_.dispatcher()]() {
client_ = std::make_shared<fidl::Client<fuchsia_ui_composition::Flatland>>(
std::move(flatland_endpoints.client), dispatcher);
completion.Signal();
});
completion.Wait();
}
~FlatlandEventLoopClientServer() {
DestroyServer();
DestroyClient();
}
async::Loop& server_loop() { return server_loop_; }
async::Loop& client_loop() { return client_loop_; }
const std::shared_ptr<Flatland>& server() const { return server_; }
const std::shared_ptr<ClientType>& client() const { return client_; }
private:
// Destroy the server on its own event loop.
void DestroyServer() {
ASSERT_TRUE(server_);
libsync::Completion completion;
async::PostTask(server_loop_.dispatcher(),
[this, &completion, server = std::move(server_)]() mutable {
server.reset();
server_loop_.Quit();
completion.Signal();
});
ASSERT_FALSE(server_);
completion.Wait();
}
// Destroy the client on its own event loop.
void DestroyClient() {
ASSERT_TRUE(client_);
libsync::Completion completion;
async::PostTask(client_loop_.dispatcher(),
[this, &completion, client = std::move(client_)]() mutable {
client.reset();
client_loop_.Quit();
completion.Signal();
});
ASSERT_FALSE(client_);
completion.Wait();
}
async::Loop server_loop_;
async::Loop client_loop_;
std::shared_ptr<Flatland> server_;
std::shared_ptr<ClientType> client_;
};
std::unique_ptr<FlatlandEventLoopClientServer> CreateFlatlandEventLoopClientServer() {
return std::make_unique<FlatlandEventLoopClientServer>(flatland_presenter_, link_system_,
uber_struct_system_);
}
std::shared_ptr<FlatlandDisplay> CreateFlatlandDisplay(uint32_t width_in_px,
uint32_t height_in_px) {
auto session_id = scheduling::GetNextSessionId();
auto display = std::make_shared<display::Display>(display::WireDisplayId{.value = 1},
width_in_px, height_in_px);
flatland_displays_.push_back({});
return FlatlandDisplay::New(
std::make_shared<utils::UnownedDispatcherHolder>(dispatcher()),
flatland_displays_.back().NewRequest(), session_id, std::move(display),
/*destroy_display_function*/ []() {}, flatland_presenter_, link_system_,
uber_struct_system_->AllocateQueueForSession(session_id));
}
fidl::InterfaceHandle<fuchsia::sysmem2::BufferCollectionToken> CreateToken() {
fuchsia::sysmem2::BufferCollectionTokenSyncPtr token;
fuchsia::sysmem2::AllocatorAllocateSharedCollectionRequest allocate_shared_request;
allocate_shared_request.set_token_request(token.NewRequest());
zx_status_t status =
sysmem_allocator_->AllocateSharedCollection(std::move(allocate_shared_request));
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem2::Node_Sync_Result sync_result;
status = token->Sync(&sync_result);
EXPECT_EQ(status, ZX_OK);
EXPECT_TRUE(sync_result.is_response());
return token;
}
// Applies the most recently scheduled session update for each session and signals the release
// fences of all Presents up to and including that update.
void ApplySessionUpdatesAndSignalFences() {
uber_struct_system_->UpdateInstances(pending_instance_updates_);
// Signal all release fences up to and including the PresentId in |pending_instance_updates_|.
for (const auto& [session_id, present_id] : pending_instance_updates_) {
auto begin = pending_release_fences_.lower_bound({session_id, 0});
auto end = pending_release_fences_.upper_bound({session_id, present_id});
for (auto fences_kv = begin; fences_kv != end; ++fences_kv) {
for (auto& event : fences_kv->second) {
event.signal(0, ZX_EVENT_SIGNALED);
}
}
pending_release_fences_.erase(begin, end);
}
pending_instance_updates_.clear();
requested_presentation_times_.clear();
}
// Gets the list of registered PresentIds for a particular |session_id|.
std::vector<scheduling::PresentId> GetRegisteredPresents(scheduling::SessionId session_id) const {
std::vector<scheduling::PresentId> present_ids;
auto begin = pending_release_fences_.lower_bound({session_id, 0});
auto end = pending_release_fences_.upper_bound({session_id + 1, 0});
for (auto fence_kv = begin; fence_kv != end; ++fence_kv) {
present_ids.push_back(fence_kv->first.present_id);
}
return present_ids;
}
// Returns true if |session_id| currently has a session update pending.
bool HasSessionUpdate(scheduling::SessionId session_id) const {
return pending_instance_updates_.count(session_id);
}
// Returns the requested presentation time for a particular |id_pair|, or std::nullopt if that
// pair has not had a presentation scheduled for it.
std::optional<zx::time> GetRequestedPresentationTime(scheduling::SchedulingIdPair id_pair) {
auto iter = requested_presentation_times_.find(id_pair);
if (iter == requested_presentation_times_.end()) {
return std::nullopt;
}
return iter->second;
}
// The parent transform must be a topology root or ComputeGlobalTopologyData() will crash.
bool IsDescendantOf(TransformHandle parent, TransformHandle child) {
auto snapshot = uber_struct_system_->Snapshot();
auto links = link_system_->GetResolvedTopologyLinks();
auto data = GlobalTopologyData::ComputeGlobalTopologyData(
snapshot, links, link_system_->GetInstanceId(), parent);
for (auto handle : data.topology_vector) {
if (handle == child) {
return true;
}
}
return false;
}
// Snapshots the UberStructSystem and fetches the UberStruct associated with |flatland|. If no
// UberStruct exists for |flatland|, returns nullptr.
std::shared_ptr<UberStruct> GetUberStruct(Flatland* flatland) {
auto snapshot = uber_struct_system_->Snapshot();
auto root = flatland->GetRoot();
auto uber_struct_kv = snapshot.find(root.GetInstanceId());
if (uber_struct_kv == snapshot.end()) {
return nullptr;
}
auto uber_struct = uber_struct_kv->second;
EXPECT_FALSE(uber_struct->local_topology.empty());
EXPECT_EQ(uber_struct->local_topology[0].handle, root);
return uber_struct;
}
// Updates all Links reachable from |root_transform|, which must be the root transform of one of
// the active Flatland instances.
//
// Tests that call this function are testing both Flatland and LinkSystem::UpdateLinkWatchers().
void UpdateLinks(TransformHandle root_transform) {
// Run the looper in case there are queued commands in, e.g., ObjectLinker.
RunLoopUntilIdle();
// This is a replica of the core render loop.
const auto snapshot = uber_struct_system_->Snapshot();
const auto links = link_system_->GetResolvedTopologyLinks();
const auto data = GlobalTopologyData::ComputeGlobalTopologyData(
snapshot, links, link_system_->GetInstanceId(), root_transform);
const auto matrices =
flatland::ComputeGlobalMatrices(data.topology_vector, data.parent_indices, snapshot);
link_system_->UpdateLinkWatchers(data.topology_vector, data.live_handles, matrices, snapshot);
link_system_->UpdateDevicePixelRatio(display_pixel_ratio_);
// Run the looper again to process any queued FIDL events (i.e., Link callbacks).
RunLoopUntilIdle();
}
void CreateViewport(Flatland* parent, Flatland* child, ContentId viewport_id,
fidl::ServerEnd<ChildViewWatcher> child_view_watcher,
fidl::ServerEnd<ParentViewportWatcher> parent_viewport_watcher) {
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
ViewportProperties properties;
properties.logical_size(SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(viewport_id, std::move(parent_token), std::move(properties),
std::move(child_view_watcher));
child->CreateView2(
std::move(child_token), fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
fuchsia_ui_composition::ViewBoundProtocols(), std::move(parent_viewport_watcher));
PRESENT(parent, true);
PRESENT(child, true);
// After View creation the child should have an associated ViewRef.
auto child_uber_struct = GetUberStruct(child);
ASSERT_NE(child_uber_struct, nullptr);
EXPECT_NE(child_uber_struct->view_ref, nullptr);
}
void SetDisplayContent(
FlatlandDisplay* display, Flatland* child,
fidl::ServerEnd<ChildViewWatcher> child_view_watcher_server_end,
fidl::ServerEnd<ParentViewportWatcher> parent_viewport_watcher_server_end) {
FX_CHECK(display);
FX_CHECK(child);
FX_CHECK(child_view_watcher_server_end);
FX_CHECK(parent_viewport_watcher_server_end);
fuchsia::ui::views::ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value, &child_token.value()));
auto present_id = scheduling::PeekNextPresentId();
EXPECT_CALL(*mock_flatland_presenter_,
ScheduleUpdateForSession(
zx::time(0), scheduling::SchedulingIdPair{display->session_id(), present_id},
true, _, _));
display->SetContent(std::move(parent_token),
fidl::NaturalToHLCPP(child_view_watcher_server_end));
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
}
// Creates an image in |flatland| with the specified |image_id| and backing properties.
// This function also returns the GlobalBufferCollectionId that will be in the ImageMetadata
// struct for that Image.
GlobalIdPair CreateImage(
Flatland* flatland, Allocator* allocator, ContentId image_id,
BufferCollectionImportExportTokens buffer_collection_import_export_tokens,
ImageProperties properties) {
const auto koid =
fsl::GetKoid(buffer_collection_import_export_tokens.export_token.value().get());
REGISTER_BUFFER_COLLECTION(allocator, buffer_collection_import_export_tokens.export_token,
CreateToken(), true);
FX_DCHECK(properties.size().has_value());
FX_DCHECK(properties.size()->width());
FX_DCHECK(properties.size()->height());
allocation::GlobalImageId global_image_id;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _))
.WillOnce(testing::Invoke([&global_image_id](const ImageMetadata& metadata,
allocation::BufferCollectionUsage usage_type) {
global_image_id = metadata.identifier;
return true;
}));
flatland->CreateImage(image_id, std::move(buffer_collection_import_export_tokens.import_token),
0, std::move(properties));
PRESENT(flatland, true);
return {.collection_id = koid, .image_id = global_image_id};
}
// Returns FlatlandError code passed returned to OnNextFrameBegin() for |flatland|.
FlatlandError GetPresentError(scheduling::SessionId session_id) {
return flatland_errors_[session_id];
}
protected:
::testing::StrictMock<MockFlatlandPresenter>* mock_flatland_presenter_;
MockBufferCollectionImporter* mock_buffer_collection_importer_;
std::shared_ptr<allocation::BufferCollectionImporter> buffer_collection_importer_;
const std::shared_ptr<UberStructSystem> uber_struct_system_;
std::shared_ptr<FlatlandPresenter> flatland_presenter_;
const std::shared_ptr<LinkSystem> link_system_;
sys::testing::ComponentContextProvider context_provider_;
private:
std::vector<
std::pair<fidl::Client<fuchsia_ui_composition::Flatland>, std::unique_ptr<EventHandler>>>
flatlands_;
std::vector<fuchsia::ui::composition::FlatlandDisplayPtr> flatland_displays_;
std::unordered_map<scheduling::SessionId, FlatlandError> flatland_errors_;
glm::vec2 display_pixel_ratio_ = kDefaultDisplayPixelRatio;
// Storage for |mock_flatland_presenter_|.
std::map<scheduling::SchedulingIdPair, std::vector<zx::event>> pending_release_fences_;
std::map<scheduling::SchedulingIdPair, zx::time> requested_presentation_times_;
std::unordered_map<scheduling::SessionId, scheduling::PresentId> pending_instance_updates_;
fuchsia::sysmem2::AllocatorSyncPtr sysmem_allocator_;
};
template <typename T>
bool ClientEndPeerExists(const fidl::ClientEnd<T>& client_end) {
auto result =
client_end.channel().wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time::infinite_past(), nullptr);
if (result == ZX_ERR_TIMED_OUT) {
return true;
}
FX_DCHECK(result == ZX_OK) << "unexpected result from channel().wait_one(): " << result;
return false;
}
bool PeerExists(const zx::unowned_channel& channel) {
auto result = channel->wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time::infinite_past(), nullptr);
if (result == ZX_ERR_TIMED_OUT) {
return true;
}
FX_DCHECK(result == ZX_OK) << "unexpected result from channel().wait_one(): " << result;
return false;
}
} // namespace
namespace flatland {
namespace test {
TEST_F(FlatlandTest, PresentShouldReturnSuccess) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, PresentErrorNoTokens) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Present, but return no tokens so the client has none left.
{
PresentArgs args;
args.present_credits_returned = 0;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// Present again, which should fail because the client has no tokens.
{
PresentArgs args;
args.expected_error = FlatlandError::kNoPresentsRemaining;
PRESENT_WITH_ARGS(flatland, std::move(args), false);
}
}
TEST_F(FlatlandTest, MultiplePresentTokensAvailable) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Return one extra present token, meaning the instance now has two.
flatland->OnNextFrameBegin(1, {});
// Present, but return no tokens so the client has only one left.
{
PresentArgs args;
args.present_credits_returned = 0;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// Present again, which should succeed because the client already has an extra token even though
// the previous PRESENT_WITH_ARGS returned none.
{
PresentArgs args;
args.present_credits_returned = 0;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// A third Present() will fail since the previous two calls consumed the two tokens.
{
PresentArgs args;
args.expected_error = FlatlandError::kNoPresentsRemaining;
PRESENT_WITH_ARGS(flatland, std::move(args), false);
}
}
TEST_F(FlatlandTest, PresentWithNoFieldsSet) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const bool kDefaultUnsquashable = false;
const zx::time kDefaultRequestedPresentationTime = zx::time(0);
fuchsia_ui_composition::PresentArgs present_args;
flatland->Present(std::move(present_args));
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(kDefaultRequestedPresentationTime,
_, kDefaultUnsquashable, _, _));
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, PresentWaitsForAcquireFences) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create two events to serve as acquire fences.
PresentArgs args;
args.acquire_fences = utils::CreateEventArray(2);
auto acquire1_copy = utils::CopyEvent(args.acquire_fences[0]);
auto acquire2_copy = utils::CopyEvent(args.acquire_fences[1]);
// Create an event to serve as a release fence.
args.release_fences = utils::CreateEventArray(1);
auto release_copy = utils::CopyEvent(args.release_fences[0]);
// Because the Present includes unsignaled acquire fences, the UberStructSystem shouldn't have any
// entries and applying session updates shouldn't signal the release fence.
PRESENT_WITH_ARGS(flatland, std::move(args), true);
auto registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
EXPECT_EQ(GetUberStruct(flatland.get()), nullptr);
EXPECT_FALSE(utils::IsEventSignalled(release_copy, ZX_EVENT_SIGNALED));
// Signal the second fence and verify the UberStructSystem doesn't update, and the release fence
// isn't signaled.
acquire2_copy.signal(0, ZX_EVENT_SIGNALED);
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
EXPECT_EQ(GetUberStruct(flatland.get()), nullptr);
EXPECT_FALSE(utils::IsEventSignalled(release_copy, ZX_EVENT_SIGNALED));
// Signal the first fence and verify that, the UberStructSystem contains an UberStruct for the
// instance, and the release fence is signaled.
acquire1_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
RunLoopUntilIdle();
// After signaling the final acquire fence (and running the loop), there is now a registered
// present. There is still no UberStruct, nor has the release fence been signalled, because
// session updates haven't been applied.
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 1ul);
EXPECT_EQ(GetUberStruct(flatland.get()), nullptr);
EXPECT_FALSE(utils::IsEventSignalled(release_copy, ZX_EVENT_SIGNALED));
ApplySessionUpdatesAndSignalFences();
// There is no longer a registered present; it was removed because it was processed when
// session updates were applied. There is now an UberStruct, and the release fence has been
// signaled.
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
EXPECT_NE(GetUberStruct(flatland.get()), nullptr);
EXPECT_TRUE(utils::IsEventSignalled(release_copy, ZX_EVENT_SIGNALED));
}
TEST_F(FlatlandTest, PresentForwardsRequestedPresentationTime) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create an event to serve as an acquire fence.
const zx::time requested_presentation_time = zx::time(123);
PresentArgs args;
args.requested_presentation_time = requested_presentation_time;
args.acquire_fences = utils::CreateEventArray(1);
auto acquire_copy = utils::CopyEvent(args.acquire_fences[0]);
// Because the Present includes acquire fences, it isn't registered with the presenter immediately
// upon `Present()`.
auto present_id = scheduling::PeekNextPresentId();
PRESENT_WITH_ARGS(flatland, std::move(args), true);
auto registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
const auto id_pair = scheduling::SchedulingIdPair({
.session_id = flatland->GetRoot().GetInstanceId(),
.present_id = present_id,
});
auto maybe_presentation_time = GetRequestedPresentationTime(id_pair);
EXPECT_FALSE(maybe_presentation_time.has_value());
// Signal the fence and ensure the Present is still registered, but now with a requested
// presentation time.
acquire_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
RunLoopUntilIdle();
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 1ul);
maybe_presentation_time = GetRequestedPresentationTime(id_pair);
EXPECT_TRUE(maybe_presentation_time.has_value());
EXPECT_EQ(maybe_presentation_time.value(), requested_presentation_time);
}
TEST_F(FlatlandTest, PresentWithSignaledFencesUpdatesImmediately) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create an event to serve as the acquire fence.
PresentArgs args;
args.acquire_fences = utils::CreateEventArray(1);
auto acquire_copy = utils::CopyEvent(args.acquire_fences[0]);
// Create an event to serve as a release fence.
args.release_fences = utils::CreateEventArray(1);
auto release_copy = utils::CopyEvent(args.release_fences[0]);
// Signal the event before the Present() call.
acquire_copy.signal(0, ZX_EVENT_SIGNALED);
// The PresentId is no longer registered because it has been applied, the UberStructSystem should
// update immediately, and the release fence should be signaled. The PRESENT macro only expects
// the ScheduleUpdateForSession() call when no acquire fences are present, but since this test
// specifically tests pre-signaled fences, the EXPECT_CALL must be added here.
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
PRESENT_WITH_ARGS(flatland, std::move(args), true);
auto registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_TRUE(registered_presents.empty());
EXPECT_NE(GetUberStruct(flatland.get()), nullptr);
EXPECT_TRUE(utils::IsEventSignalled(release_copy, ZX_EVENT_SIGNALED));
}
TEST_F(FlatlandTest, PresentsUpdateInCallOrder) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create an event to serve as the acquire fence for the first Present().
PresentArgs args1;
args1.acquire_fences = utils::CreateEventArray(1);
auto acquire1_copy = utils::CopyEvent(args1.acquire_fences[0]);
// Create an event to serve as a release fence.
args1.release_fences = utils::CreateEventArray(1);
auto release1_copy = utils::CopyEvent(args1.release_fences[0]);
// Present, but do not signal the fence, and ensure Present is registered, the UberStructSystem is
// empty, and the release fence is unsignaled.
PRESENT_WITH_ARGS(flatland, std::move(args1), true);
// No presents have been registered since the acquire fence hasn't been signaled yet.
auto registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
EXPECT_EQ(GetUberStruct(flatland.get()), nullptr);
EXPECT_FALSE(utils::IsEventSignalled(release1_copy, ZX_EVENT_SIGNALED));
// Create a transform and make it the root.
const TransformId kId = {1};
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
// Create another event to serve as the acquire fence for the second Present().
PresentArgs args2;
args2.acquire_fences = utils::CreateEventArray(1);
auto acquire2_copy = utils::CopyEvent(args2.acquire_fences[0]);
// Create an event to serve as a release fence.
args2.release_fences = utils::CreateEventArray(1);
auto release2_copy = utils::CopyEvent(args2.release_fences[0]);
// Present, but do not signal the fence, and ensure there are two Presents registered, but the
// UberStructSystem is still empty and both release fences are unsignaled.
PRESENT_WITH_ARGS(flatland, std::move(args2), true);
// No presents have been registered since neither of the acquire fences have been signaled yet.
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
EXPECT_EQ(GetUberStruct(flatland.get()), nullptr);
EXPECT_FALSE(utils::IsEventSignalled(release1_copy, ZX_EVENT_SIGNALED));
EXPECT_FALSE(utils::IsEventSignalled(release2_copy, ZX_EVENT_SIGNALED));
// Signal the fence for the second Present(). Since the first one is not done, there should still
// be no Presents registered, no UberStruct for the instance, and neither fence should be
// signaled.
acquire2_copy.signal(0, ZX_EVENT_SIGNALED);
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 0ul);
EXPECT_EQ(GetUberStruct(flatland.get()), nullptr);
EXPECT_FALSE(utils::IsEventSignalled(release1_copy, ZX_EVENT_SIGNALED));
EXPECT_FALSE(utils::IsEventSignalled(release2_copy, ZX_EVENT_SIGNALED));
// Signal the fence for the first Present(). This should trigger both Presents(), resulting no
// registered Presents and an UberStruct with a 2-element topology: the local root, and kId.
acquire1_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _)).Times(2);
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
registered_presents = GetRegisteredPresents(flatland->GetRoot().GetInstanceId());
EXPECT_TRUE(registered_presents.empty());
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_NE(uber_struct, nullptr);
EXPECT_EQ(uber_struct->local_topology.size(), 2ul);
EXPECT_TRUE(utils::IsEventSignalled(release1_copy, ZX_EVENT_SIGNALED));
EXPECT_TRUE(utils::IsEventSignalled(release2_copy, ZX_EVENT_SIGNALED));
}
TEST_F(FlatlandTest, SetHitRegionsErrorTest) {
// Zero is not a valid transform ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
const TransformId kId = {0};
flatland->SetHitRegions(kId, {});
PRESENT(flatland, /*expect_success=*/false);
}
// Transform ID should be present.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
const TransformId kId = {42};
flatland->SetHitRegions(kId, {});
PRESENT(flatland, /*expect_success=*/false);
}
auto interaction = fuchsia_ui_composition::HitTestInteraction::kDefault;
const TransformId kId = {1};
// Height should be non-negative.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
fuchsia_math::RectF rect = {0, 0, 0, -1};
fuchsia_ui_composition::HitRegion region = {rect, interaction};
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetHitRegions(kId, {region});
PRESENT(flatland, /*expect_success=*/false);
}
// Width should be non-negative.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
fuchsia_math::RectF rect = {0, 0, -1, 0};
fuchsia_ui_composition::HitRegion region = {rect, interaction};
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetHitRegions(kId, {region});
PRESENT(flatland, /*expect_success=*/false);
}
// Negative origin should succeed.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
fuchsia_math::RectF rect = {-1, -1, 0, 0};
fuchsia_ui_composition::HitRegion region = {rect, interaction};
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetHitRegions(kId, {region});
PRESENT(flatland, /*expect_success=*/true);
}
// Empty hit region vector should succeed.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetHitRegions(kId, {});
PRESENT(flatland, /*expect_success=*/true);
}
// Valid hit region vector should succeed.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
fuchsia_math::RectF rect = {0, 0, 10, 10};
fuchsia_ui_composition::HitRegion region = {rect, interaction};
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetHitRegions(kId, {region});
PRESENT(flatland, /*expect_success=*/true);
}
// Consecutive SetRootTransforms with the same transform should work.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
fuchsia_math::RectF rect = {0, 0, 0, 0};
fuchsia_ui_composition::HitRegion region = {rect, interaction};
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetHitRegions(kId, {region});
PRESENT(flatland, /*expect_success=*/true);
}
}
TEST_F(FlatlandTest, SetDebugNameAddsPrefixToLogs) {
class TestErrorReporter : public scenic_impl::ErrorReporter {
public:
TestErrorReporter(std::optional<std::string>* last_error_log)
: reported_error(last_error_log) {}
std::optional<std::string>* reported_error = nullptr;
private:
// |scenic_impl::ErrorReporter|
void ReportError(fuchsia_logging::LogSeverity severity, std::string error_string) override {
*reported_error = error_string;
}
};
const TransformId kInvalidId = {0};
// No prefix in errors by default.
{
std::optional<std::string> error_log;
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetErrorReporter(std::make_unique<TestErrorReporter>(&error_log));
flatland->CreateTransform(kInvalidId);
PRESENT(flatland, false);
ASSERT_TRUE(error_log.has_value());
EXPECT_EQ("CreateTransform called with transform_id=0", *error_log);
}
// SetDebugName() adds a prefix.
{
std::optional<std::string> error_log;
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetErrorReporter(std::make_unique<TestErrorReporter>(&error_log));
flatland->SetDebugName("test_client");
flatland->CreateTransform(kInvalidId);
PRESENT(flatland, false);
ASSERT_TRUE(error_log.has_value());
EXPECT_EQ("Flatland client(test_client): CreateTransform called with transform_id=0",
*error_log);
}
// ErrorReporter logs the prefix of multiple flatland clients correctly.
{
std::optional<std::string> error_log_a;
std::optional<std::string> error_log_b;
std::shared_ptr<Flatland> flatland_a = CreateFlatland();
std::shared_ptr<Flatland> flatland_b = CreateFlatland();
flatland_a->SetErrorReporter(std::make_unique<TestErrorReporter>(&error_log_a));
flatland_b->SetErrorReporter(std::make_unique<TestErrorReporter>(&error_log_b));
flatland_a->SetDebugName("test_client");
flatland_b->SetDebugName("test_client1");
flatland_a->CreateTransform(kInvalidId);
flatland_b->CreateTransform(kInvalidId);
PRESENT(flatland_a, false);
PRESENT(flatland_b, false);
ASSERT_TRUE(error_log_a.has_value());
ASSERT_TRUE(error_log_b.has_value());
EXPECT_EQ("Flatland client(test_client): CreateTransform called with transform_id=0",
*error_log_a);
EXPECT_EQ("Flatland client(test_client1): CreateTransform called with transform_id=0",
*error_log_b);
}
}
TEST_F(FlatlandTest, SetDebugNameAddsDebugNameToUberStruct) {
// debug_name is empty when SetDebugName() is not called.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
PRESENT(flatland, true);
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_TRUE(uber_struct->debug_name.empty());
}
// UberStruct sees value of SetDebugName() after Present().
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetDebugName("test_client");
PRESENT(flatland, true);
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ("test_client", uber_struct->debug_name);
}
// Clear() resets the debug_name member of UberStruct.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetDebugName("test_client");
PRESENT(flatland, true);
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ("test_client", uber_struct->debug_name);
flatland->Clear();
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
EXPECT_TRUE(uber_struct->debug_name.empty());
}
}
TEST_F(FlatlandTest, CreateAndReleaseTransformValidCases) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const TransformId kId1 = {1};
const TransformId kId2 = {2};
// Create two transforms.
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
PRESENT(flatland, true);
// Clear, then create two transforms in the other order.
flatland->Clear();
flatland->CreateTransform(kId2);
flatland->CreateTransform(kId1);
PRESENT(flatland, true);
// Clear, create and release transforms, non-overlapping.
flatland->Clear();
flatland->CreateTransform(kId1);
flatland->ReleaseTransform(kId1);
flatland->CreateTransform(kId2);
flatland->ReleaseTransform(kId2);
PRESENT(flatland, true);
// Clear, create and release transforms, nested.
flatland->Clear();
flatland->CreateTransform(kId2);
flatland->CreateTransform(kId1);
flatland->ReleaseTransform(kId1);
flatland->ReleaseTransform(kId2);
PRESENT(flatland, true);
// Reuse the same id, legally, in a single present call.
flatland->CreateTransform(kId1);
flatland->ReleaseTransform(kId1);
flatland->CreateTransform(kId1);
flatland->Clear();
flatland->CreateTransform(kId1);
PRESENT(flatland, true);
// Create and clear, overlapping, with multiple present calls.
flatland->Clear();
flatland->CreateTransform(kId2);
PRESENT(flatland, true);
flatland->CreateTransform(kId1);
flatland->ReleaseTransform(kId2);
PRESENT(flatland, true);
flatland->ReleaseTransform(kId1);
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, CreateAndReleaseTransformErrorCases) {
const TransformId kId1 = {1};
const TransformId kId2 = {2};
// Zero is not a valid transform id.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform({0});
PRESENT(flatland, false);
}
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseTransform({0});
PRESENT(flatland, false);
}
// Double creation is an error.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId1);
PRESENT(flatland, false);
}
// Releasing a non-existent transform is an error.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseTransform(kId2);
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, AddAndRemoveChildValidCases) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const TransformId kIdParent = {1};
const TransformId kIdChild1 = {2};
const TransformId kIdChild2 = {3};
const TransformId kIdGrandchild = {4};
flatland->CreateTransform(kIdParent);
flatland->CreateTransform(kIdChild1);
flatland->CreateTransform(kIdChild2);
flatland->CreateTransform(kIdGrandchild);
PRESENT(flatland, true);
// Add and remove.
flatland->AddChild(kIdParent, kIdChild1);
flatland->RemoveChild(kIdParent, kIdChild1);
PRESENT(flatland, true);
// Add two children.
flatland->AddChild(kIdParent, kIdChild1);
flatland->AddChild(kIdParent, kIdChild2);
PRESENT(flatland, true);
// Remove two children.
flatland->RemoveChild(kIdParent, kIdChild1);
flatland->RemoveChild(kIdParent, kIdChild2);
PRESENT(flatland, true);
// Add two-deep hierarchy.
flatland->AddChild(kIdParent, kIdChild1);
flatland->AddChild(kIdChild1, kIdGrandchild);
PRESENT(flatland, true);
// Add sibling.
flatland->AddChild(kIdParent, kIdChild2);
PRESENT(flatland, true);
// Add shared grandchild (deadly diamond dependency).
flatland->AddChild(kIdChild2, kIdGrandchild);
PRESENT(flatland, true);
// Remove original deep-hierarchy.
flatland->RemoveChild(kIdChild1, kIdGrandchild);
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, AddAndRemoveChildErrorCases) {
const TransformId kIdParent = {1};
const TransformId kIdChild = {2};
const TransformId kIdNotCreated = {3};
// Setup.
auto SetupFlatland = [&]() {
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kIdParent);
flatland->CreateTransform(kIdChild);
flatland->AddChild(kIdParent, kIdChild);
return flatland;
};
{
auto flatland = SetupFlatland();
PRESENT(flatland, true);
}
// Zero is not a valid transform id.
{
auto flatland = SetupFlatland();
flatland->AddChild({0}, {0});
PRESENT(flatland, false);
}
{
auto flatland = SetupFlatland();
flatland->AddChild(kIdParent, {0});
PRESENT(flatland, false);
}
{
auto flatland = SetupFlatland();
flatland->AddChild({0}, kIdChild);
PRESENT(flatland, false);
}
// Child does not exist.
{
auto flatland = SetupFlatland();
flatland->AddChild(kIdParent, kIdNotCreated);
PRESENT(flatland, false);
}
{
auto flatland = SetupFlatland();
flatland->RemoveChild(kIdParent, kIdNotCreated);
PRESENT(flatland, false);
}
// Parent does not exist.
{
auto flatland = SetupFlatland();
flatland->AddChild(kIdNotCreated, kIdChild);
PRESENT(flatland, false);
}
{
auto flatland = SetupFlatland();
flatland->RemoveChild(kIdNotCreated, kIdChild);
PRESENT(flatland, false);
}
// Child is already a child of parent->
{
auto flatland = SetupFlatland();
flatland->AddChild(kIdParent, kIdChild);
PRESENT(flatland, false);
}
// Both nodes exist, but not in the correct relationship.
{
auto flatland = SetupFlatland();
flatland->RemoveChild(kIdChild, kIdParent);
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, ReplaceChildren) {
const TransformId kIdParent = {1};
const TransformId kIdChild1 = {2};
const TransformId kIdChild2 = {3};
const TransformId kIdChild3 = {4};
const TransformId kIdChild4 = {5};
// Setup.
auto SetupFlatland = [&]() {
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kIdParent);
flatland->CreateTransform(kIdChild1);
flatland->CreateTransform(kIdChild2);
flatland->CreateTransform(kIdChild3);
flatland->CreateTransform(kIdChild4);
flatland->AddChild(kIdParent, kIdChild1);
return flatland;
};
// ReplaceChildren fails with duplicate new children.
{
auto flatland = SetupFlatland();
flatland->ReplaceChildren(kIdParent, {kIdChild2, kIdChild2, kIdChild3});
PRESENT(flatland, false);
}
{
auto flatland = SetupFlatland();
flatland->ReplaceChildren(kIdParent, {kIdChild2, kIdChild3, kIdChild4});
PRESENT(flatland, true);
}
}
// Test that Transforms can be children to multiple different parents.
TEST_F(FlatlandTest, MultichildUsecase) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const TransformId kIdParent1 = {1};
const TransformId kIdParent2 = {2};
const TransformId kIdChild1 = {3};
const TransformId kIdChild2 = {4};
const TransformId kIdChild3 = {5};
// Setup
flatland->CreateTransform(kIdParent1);
flatland->CreateTransform(kIdParent2);
flatland->CreateTransform(kIdChild1);
flatland->CreateTransform(kIdChild2);
flatland->CreateTransform(kIdChild3);
PRESENT(flatland, true);
// Add all children to first parent->
flatland->AddChild(kIdParent1, kIdChild1);
flatland->AddChild(kIdParent1, kIdChild2);
flatland->AddChild(kIdParent1, kIdChild3);
PRESENT(flatland, true);
// Add all children to second parent->
flatland->AddChild(kIdParent2, kIdChild1);
flatland->AddChild(kIdParent2, kIdChild2);
flatland->AddChild(kIdParent2, kIdChild3);
PRESENT(flatland, true);
}
// Test that when a transform has multiple parents, that
// the number of nodes in the uber_structs global topology
// is what we would expect.
TEST_F(FlatlandTest, MultichildTest2) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const TransformId kIdRoot = {1};
const TransformId kIdParent1 = {2};
const TransformId kIdParent2 = {3};
const TransformId kIdChild = {4};
// Create the transforms.
flatland->CreateTransform(kIdRoot);
flatland->CreateTransform(kIdParent1);
flatland->CreateTransform(kIdParent2);
flatland->CreateTransform(kIdChild);
PRESENT(flatland, true);
// Setup the diamond parent hierarchy.
flatland->SetRootTransform(kIdRoot);
flatland->AddChild(kIdRoot, kIdParent1);
flatland->AddChild(kIdRoot, kIdParent2);
flatland->AddChild(kIdParent1, kIdChild);
flatland->AddChild(kIdParent2, kIdChild);
PRESENT(flatland, true);
// The transform kIdChild should be doubly listed
// in the uber struct.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.size(), 6U);
}
// Test that Present() fails if it detects a graph cycle.
TEST_F(FlatlandTest, CycleDetector) {
const TransformId kId1 = {1};
const TransformId kId2 = {2};
const TransformId kId3 = {3};
const TransformId kId4 = {4};
// Create an immediate cycle.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kId1);
flatland->AddChild(kId1, kId1);
PRESENT(flatland, false);
}
// Create a legal chain of depth one.
// Then, create a cycle of length 2.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->Clear();
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
flatland->AddChild(kId1, kId2);
PRESENT(flatland, true);
flatland->AddChild(kId2, kId1);
PRESENT(flatland, false);
}
// Create two legal chains of length one.
// Then, connect each chain into a cycle of length four.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->Clear();
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
flatland->CreateTransform(kId3);
flatland->CreateTransform(kId4);
flatland->AddChild(kId1, kId2);
flatland->AddChild(kId3, kId4);
PRESENT(flatland, true);
flatland->AddChild(kId2, kId3);
flatland->AddChild(kId4, kId1);
PRESENT(flatland, false);
}
// Create a cycle, where the root is not involved in the cycle.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->Clear();
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
flatland->CreateTransform(kId3);
flatland->CreateTransform(kId4);
flatland->AddChild(kId1, kId2);
flatland->AddChild(kId2, kId3);
flatland->AddChild(kId3, kId2);
flatland->AddChild(kId3, kId4);
flatland->SetRootTransform(kId1);
flatland->ReleaseTransform(kId1);
flatland->ReleaseTransform(kId2);
flatland->ReleaseTransform(kId3);
flatland->ReleaseTransform(kId4);
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetRootTransform) {
const TransformId kId1 = {1};
const TransformId kIdNotCreated = {2};
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kId1);
PRESENT(flatland, true);
// Even with no root transform, so clearing it is not an error.
flatland->SetRootTransform({0});
PRESENT(flatland, true);
flatland->SetRootTransform(kId1);
PRESENT(flatland, true);
// Setting the root to a non-existent transform does not clear the root, which means the local
// topology will contain two handles: the "local root" and kId1.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.size(), 2ul);
// Releasing the root is allowed, though it will remain in the hierarchy until reset.
flatland->ReleaseTransform(kId1);
PRESENT(flatland, true);
// Clearing the root after release is also allowed.
flatland->SetRootTransform({0});
PRESENT(flatland, true);
// Setting the root to a released transform is not allowed.
flatland->SetRootTransform(kId1);
PRESENT(flatland, false);
}
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetRootTransform(kIdNotCreated);
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetTranslationErrorCases) {
const TransformId kIdNotCreated = {1};
// Zero is not a valid transform ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetTranslation({0}, {1, 2});
PRESENT(flatland, false);
}
// Transform does not exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetTranslation(kIdNotCreated, {1, 2});
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetOrientationErrorCases) {
const TransformId kIdNotCreated = {1};
// Zero is not a valid transform ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetOrientation({0}, Orientation::kCcw90Degrees);
PRESENT(flatland, false);
}
// Transform does not exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetOrientation(kIdNotCreated, Orientation::kCcw90Degrees);
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetScaleErrorCases) {
const TransformId kIdNotCreated = {1};
// Zero is not a valid transform ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetScale({0}, {1, 2});
PRESENT(flatland, false);
}
// Transform does not exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetScale(kIdNotCreated, {1, 2});
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetImageDestinationSizeErrorCases) {
const ContentId kIdNotCreated = {1};
// Zero is not a valid content ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageDestinationSize({0}, {1, 2});
PRESENT(flatland, false);
}
// Content does not exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageDestinationSize(kIdNotCreated, {1, 2});
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetImageBlendFunctionErrorCases) {
const ContentId kIdNotCreated = {1};
// Zero is not a valid content ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageBlendMode({0}, BlendMode::kReplace());
PRESENT(flatland, false);
}
// Content does not exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageBlendMode(kIdNotCreated, BlendMode::kReplace());
PRESENT(flatland, false);
}
}
class FlatlandParameterizedTest : public FlatlandTest,
public ::testing::WithParamInterface<BlendMode> {};
// Make sure that the data for setting the blend mode gets passed to
// the uberstruct correctly.
TEST_P(FlatlandParameterizedTest, SetImageBlendModeUberstructTest) {
const BlendMode blend_mode_param = GetParam();
const ContentId kImageId1 = {1};
const ContentId kImageId2 = {2};
const TransformId kTransformId1 = {3};
const TransformId kTransformId2 = {4};
std::shared_ptr<Flatland> flatland = CreateFlatland();
std::shared_ptr<Allocator> allocator = CreateAllocator();
// Create constants.
const uint32_t kImageWidth = 50;
const uint32_t kImageHeight = 100;
// Setup first image to be opaque.
{
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(fuchsia_math::SizeU{kImageWidth, kImageHeight});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId1, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId1);
flatland->SetRootTransform(kTransformId1);
flatland->SetContent(kTransformId1, kImageId1);
flatland->SetImageBlendMode(kImageId1, BlendMode::kReplace());
PRESENT(flatland, true);
}
// Create a second image to be transparent.
{
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(fuchsia_math::SizeU{kImageWidth, kImageHeight});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId2, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId2);
flatland->AddChild(kTransformId1, kTransformId2);
flatland->SetContent(kTransformId2, kImageId2);
flatland->SetImageBlendMode(kImageId2, blend_mode_param);
PRESENT(flatland, true);
}
// Get the first image content handle
const auto maybe_image_1_handle = flatland->GetContentHandle(kImageId1);
ASSERT_TRUE(maybe_image_1_handle.has_value());
const auto image_1_handle = maybe_image_1_handle.value();
// Get the second image content handle
const auto maybe_image_2_handle = flatland->GetContentHandle(kImageId2);
ASSERT_TRUE(maybe_image_2_handle.has_value());
const auto image_2_handle = maybe_image_2_handle.value();
// Now find the data in the uber struct.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, image_2_handle);
// Grab the metadatas for each handle.
auto image_1_kv = uber_struct->images.find(image_1_handle);
EXPECT_NE(image_1_kv, uber_struct->images.end());
auto image_2_kv = uber_struct->images.find(image_2_handle);
EXPECT_NE(image_2_kv, uber_struct->images.end());
// Make sure the opacity fields are set properly.
EXPECT_TRUE(image_1_kv->second.blend_mode == BlendMode::kReplace());
EXPECT_TRUE(image_2_kv->second.blend_mode == blend_mode_param);
}
INSTANTIATE_TEST_SUITE_P(SetBlendModes, FlatlandParameterizedTest,
::testing::Values(BlendMode::kPremultipliedAlpha(),
BlendMode::kStraightAlpha()));
TEST_F(FlatlandTest, SetImageFlipErrorCases) {
const ContentId kIdNotCreated = {1};
// Zero is not a valid content ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageFlip({0}, fuchsia_ui_composition::ImageFlip::kLeftRight);
PRESENT(flatland, false);
}
// Content does not exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageFlip(kIdNotCreated, fuchsia_ui_composition::ImageFlip::kLeftRight);
PRESENT(flatland, false);
}
}
// Make sure that the data for setting the image flip gets passed to the uberstruct correctly.
TEST_F(FlatlandTest, SetImageFlipUberstructTest) {
const ContentId kImageId1 = {1};
const ContentId kImageId2 = {2};
const TransformId kTransformId1 = {3};
const TransformId kTransformId2 = {4};
std::shared_ptr<Flatland> flatland = CreateFlatland();
std::shared_ptr<Allocator> allocator = CreateAllocator();
// Create constants.
const uint32_t kImageWidth = 50;
const uint32_t kImageHeight = 100;
// Setup first image to have default NONE flip property.
{
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(fuchsia_math::SizeU{kImageWidth, kImageHeight});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId1, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId1);
flatland->SetRootTransform(kTransformId1);
flatland->SetContent(kTransformId1, kImageId1);
PRESENT(flatland, true);
}
// Create a second image to be flipped up-down.
{
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(fuchsia_math::SizeU{kImageWidth, kImageHeight});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId2, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId2);
flatland->AddChild(kTransformId1, kTransformId2);
flatland->SetContent(kTransformId2, kImageId2);
flatland->SetImageFlip(kImageId2, fuchsia_ui_composition::ImageFlip::kUpDown);
PRESENT(flatland, true);
}
// Get the first image content handle
const auto maybe_image_1_handle = flatland->GetContentHandle(kImageId1);
ASSERT_TRUE(maybe_image_1_handle.has_value());
const auto image_1_handle = maybe_image_1_handle.value();
// Get the second image content handle
const auto maybe_image_2_handle = flatland->GetContentHandle(kImageId2);
ASSERT_TRUE(maybe_image_2_handle.has_value());
const auto image_2_handle = maybe_image_2_handle.value();
// Now find the data in the uber struct.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, image_2_handle);
// Grab the metadatas for each handle.
auto image_1_kv = uber_struct->images.find(image_1_handle);
EXPECT_NE(image_1_kv, uber_struct->images.end());
auto image_2_kv = uber_struct->images.find(image_2_handle);
EXPECT_NE(image_2_kv, uber_struct->images.end());
// Make sure the flip fields are set properly.
EXPECT_TRUE(image_1_kv->second.flip == fuchsia_ui_composition::ImageFlip::kNone);
EXPECT_TRUE(image_2_kv->second.flip == fuchsia_ui_composition::ImageFlip::kUpDown);
}
// Test that changing geometric transform properties affects the local matrix of Transforms.
TEST_F(FlatlandTest, SetGeometricTransformProperties) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create two Transforms to ensure properties are local to individual Transforms.
const TransformId kId1 = {1};
const TransformId kId2 = {2};
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
flatland->SetRootTransform(kId1);
flatland->AddChild(kId1, kId2);
PRESENT(flatland, true);
// Get the TransformHandles for kId1 and kId2.
auto uber_struct = GetUberStruct(flatland.get());
ASSERT_EQ(uber_struct->local_topology.size(), 3ul);
ASSERT_EQ(uber_struct->local_topology[0].handle, flatland->GetRoot());
const auto handle1 = uber_struct->local_topology[1].handle;
const auto handle2 = uber_struct->local_topology[2].handle;
// The local topology will always have 3 transforms: the local root, kId1, and kId2. With no
// properties set, there will be no local matrices.
uber_struct = GetUberStruct(flatland.get());
EXPECT_TRUE(uber_struct->local_matrices.empty());
// Set one transform property for each node.
// Set translation on first transform.
flatland->SetTranslation(kId1, {1, 2});
// Set scale on second transform.
flatland->SetScale(kId2, {2.f, 3.f});
PRESENT(flatland, true);
// The two handles should have the expected matrices.
uber_struct = GetUberStruct(flatland.get());
EXPECT_MATRIX(uber_struct, handle1, glm::translate(glm::mat3(), {1, 2}));
EXPECT_MATRIX(uber_struct, handle2, glm::scale(glm::mat3(), {2.f, 3.f}));
// Fill out the remaining properties on both transforms.
flatland->SetScale(kId1, {4.f, 5.f});
flatland->SetOrientation(kId1, Orientation::kCcw90Degrees);
flatland->SetOrientation(kId2, Orientation::kCcw270Degrees);
flatland->SetTranslation(kId2, {6, 7});
PRESENT(flatland, true);
// Verify the new properties were applied in the correct orders.
uber_struct = GetUberStruct(flatland.get());
// The way the glm function calls handle translation/scale/rotation is
// a bit unintuitive. If you call glm::scale(translation_matrix, vec2),
// this may appear as if we are incorrectly translating first and scaling
// second, but glm will apply them in the order (translation * scale * (point))
// which is indeed the ordering that we want. In other words, the built-in glm
// calls *right multiply* instead of *left multiply*.
glm::mat3 matrix1 = glm::mat3();
matrix1 = glm::translate(matrix1, {1, 2});
matrix1 = glm::rotate(matrix1, utils::GetOrientationAngle(Orientation::kCcw90Degrees));
matrix1 = glm::scale(matrix1, {4.f, 5.f});
EXPECT_MATRIX(uber_struct, handle1, matrix1);
glm::mat3 matrix2 = glm::mat3();
matrix2 = glm::translate(matrix2, {6, 7});
matrix2 = glm::rotate(matrix2, utils::GetOrientationAngle(Orientation::kCcw270Degrees));
matrix2 = glm::scale(matrix2, {2.f, 3.f});
EXPECT_MATRIX(uber_struct, handle2, matrix2);
}
// Ensure that local matrix data is only cleaned up when a Transform is completely unreferenced,
// meaning no Transforms reference it as a child->
TEST_F(FlatlandTest, MatrixReleasesWhenTransformNotReferenced) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create two Transforms to ensure properties are local to individual Transforms.
const TransformId kId1 = {1};
const TransformId kId2 = {2};
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
flatland->SetRootTransform(kId1);
flatland->AddChild(kId1, kId2);
PRESENT(flatland, true);
// Get the TransformHandles for kId1 and kId2.
auto uber_struct = GetUberStruct(flatland.get());
ASSERT_EQ(uber_struct->local_topology.size(), 3ul);
ASSERT_EQ(uber_struct->local_topology[0].handle, flatland->GetRoot());
const auto handle1 = uber_struct->local_topology[1].handle;
const auto handle2 = uber_struct->local_topology[2].handle;
// Set a geometric property on kId1.
flatland->SetTranslation(kId1, {1, 2});
PRESENT(flatland, true);
// Only handle1 should have a local matrix.
uber_struct = GetUberStruct(flatland.get());
EXPECT_MATRIX(uber_struct, handle1, glm::translate(glm::mat3(), {1, 2}));
// Release kId1, but ensure its matrix stays around.
flatland->ReleaseTransform(kId1);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
EXPECT_MATRIX(uber_struct, handle1, glm::translate(glm::mat3(), {1, 2}));
// Clear kId1 as the root transform, which should clear the matrix.
flatland->SetRootTransform({0});
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
EXPECT_TRUE(uber_struct->local_matrices.empty());
}
TEST_F(FlatlandTest, CreateViewReplaceWithoutConnection) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end));
PRESENT(flatland, true);
ViewportCreationToken parent_token2;
ViewCreationToken child_token2;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token2.value(), &child_token2.value()));
auto [parent_viewport_watcher_client_end2, parent_viewport_watcher_server_end2] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token2),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end2));
RunLoopUntilIdle();
// Until Present() is called, the previous ParentViewportWatcher is not unbound.
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
PRESENT(flatland, true);
EXPECT_FALSE(ClientEndPeerExists(parent_viewport_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
}
TEST_F(FlatlandTest, ParentViewportWatcherReplaceWithConnection) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
CreateViewport(parent.get(), child.get(), kLinkId1, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
// Don't use the helper function for the second link to test when the previous links are closed.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Creating the new ParentViewportWatcher doesn't invalidate either of the old links until
// Present() is called on the child->
auto [parent_viewport_watcher_client_end2, parent_viewport_watcher_server_end2] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end2));
RunLoopUntilIdle();
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
// Present() replaces the original ParentViewportWatcher, which also results in the invalidation
// of both ends of the original link.
PRESENT(child, true);
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_FALSE(ClientEndPeerExists(parent_viewport_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
}
TEST_F(FlatlandTest, ParentViewportWatcherUnbindsOnParentDeath) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end));
PRESENT(flatland, true);
parent_token.value().reset();
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(parent_viewport_watcher_client_end));
}
TEST_F(FlatlandTest, ParentViewportWatcherUnbindsImmediatelyWithInvalidToken) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewCreationToken child_token;
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end));
// The link will be unbound even before Present() is called.
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(parent_viewport_watcher_client_end));
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ReleaseViewFailsWithoutLink) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseView();
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ReleaseViewSucceedsWithLink) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end));
PRESENT(flatland, true);
// Killing the peer token does not prevent the instance from releasing view.
parent_token.value().reset();
RunLoopUntilIdle();
flatland->ReleaseView();
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, CreateViewSuccceedsAfterReleaseView) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end));
PRESENT(flatland, true);
// Killing the peer token does not prevent the instance from releasing view.
parent_token.value().reset();
RunLoopUntilIdle();
ViewportCreationToken parent_token2;
ViewCreationToken child_token2;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token2.value(), &child_token2.value()));
auto [parent_viewport_watcher_client_end2, parent_viewport_watcher_server_end2] =
fidl::Endpoints<ParentViewportWatcher>::Create();
flatland->CreateView2(std::move(child_token2),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end2));
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, ChildViewWatcherUnbindsOnChildDeath) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport(kLinkId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, true);
child_token.value().reset();
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end));
}
TEST_F(FlatlandTest, ChildViewWatcherUnbindsImmediatelyWithInvalidToken) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport(kLinkId1, std::move(parent_token), {},
std::move(child_view_watcher_server_end));
// The link will be unbound even before Present() is called.
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end));
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ChildViewWatcherFailsIdIsZero) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport({0}, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ChildViewWatcherFailsNoLogicalSize) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
flatland->CreateViewport({0}, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ChildViewWatcherFailsInvalidLogicalSize) {
ViewportCreationToken parent_token;
ViewCreationToken child_token;
// The X value must be positive.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{0, kDefaultSize});
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport({0}, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, false);
}
// The Y value must be positive.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
ViewportProperties properties2;
properties2.logical_size(fuchsia_math::SizeU{kDefaultSize, 0});
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport({0}, std::move(parent_token), std::move(properties2),
std::move(child_view_watcher_server_end));
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, ChildViewAutomaticallyClipsBounds) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId1 = {1};
ViewportProperties properties;
// Create the viewport and check the uberstruct for the clip bounds.
{
const int32_t kWidth = 300;
const int32_t kHeight = 500;
properties.logical_size(fuchsia_math::SizeU{kWidth, kHeight});
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport(kLinkId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, true);
auto maybe_transform = flatland->GetContentHandle(kLinkId1);
EXPECT_TRUE(maybe_transform);
auto transform = *maybe_transform;
auto uber_struct = GetUberStruct(flatland.get());
auto clip_itr = uber_struct->local_clip_regions.find(transform);
EXPECT_NE(clip_itr, uber_struct->local_clip_regions.end());
auto clip_region = clip_itr->second;
EXPECT_EQ(clip_region,
TransformClipRegion({.x = 0, .y = 0, .width = kWidth, .height = kHeight}));
}
// Change the bounds via a call to |SetViewProperties| and make sure they've changed.
{
const int32_t kWidth = 900;
const int32_t kHeight = 700;
properties.logical_size(fuchsia_math::SizeU{kWidth, kHeight});
flatland->SetViewportProperties(kLinkId1, std::move(properties));
PRESENT(flatland, true);
auto maybe_transform = flatland->GetContentHandle(kLinkId1);
EXPECT_TRUE(maybe_transform);
auto transform = *maybe_transform;
auto uber_struct = GetUberStruct(flatland.get());
auto clip_itr = uber_struct->local_clip_regions.find(transform);
EXPECT_NE(clip_itr, uber_struct->local_clip_regions.end());
auto clip_region = clip_itr->second;
EXPECT_EQ(clip_region,
TransformClipRegion({.x = 0, .y = 0, .width = kWidth, .height = kHeight}));
}
}
TEST_F(FlatlandTest, ViewportClippingPersistsAcrossInstances) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Create and link the two instances.
const TransformId kId1 = {1};
parent->CreateTransform(kId1);
parent->SetRootTransform(kId1);
const ContentId kLinkId = {1};
ViewportProperties properties;
const int32_t kViewportWidth = 75;
const int32_t kViewportHeight = 325;
properties.logical_size(fuchsia_math::SizeU{kViewportWidth, kViewportHeight});
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kId1, kLinkId);
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
PRESENT(parent, true);
PRESENT(child, true);
EXPECT_TRUE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
// Calculate the global clip regions of the parent and child flatland instances, and ensure
// that the root of the child instance has a global clip region equivalent to that of the
// logical size of the parent viewport's properties.
const auto snapshot = uber_struct_system_->Snapshot();
const auto links = link_system_->GetResolvedTopologyLinks();
const auto link_system_id = link_system_->GetInstanceId();
const auto topology_data = flatland::GlobalTopologyData::ComputeGlobalTopologyData(
snapshot, links, link_system_id, parent->GetRoot());
const auto global_matrices = flatland::ComputeGlobalMatrices(
topology_data.topology_vector, topology_data.parent_indices, snapshot);
const auto global_clip_regions = ComputeGlobalTransformClipRegions(
topology_data.topology_vector, topology_data.parent_indices, global_matrices, snapshot);
auto child_root_handle = topology_data.topology_vector[3];
EXPECT_EQ(child->GetRoot(), child_root_handle);
auto child_root_clip = global_clip_regions[3];
EXPECT_EQ(
child_root_clip,
TransformClipRegion({.x = 0, .y = 0, .width = kViewportWidth, .height = kViewportHeight}));
}
TEST_F(FlatlandTest, DefaultHitRegion_IsInfinite) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const auto session_id = flatland->GetSessionId();
const TransformId kId1 = {1};
const TransformHandle handle1 = TransformHandle(session_id, 1);
flatland->CreateTransform(kId1);
flatland->SetRootTransform(kId1);
PRESENT(flatland, true);
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
ASSERT_EQ(hit_regions.size(), 1u);
ASSERT_EQ(hit_regions[handle1].size(), 1u);
EXPECT_FALSE(hit_regions[handle1][0].is_finite());
}
}
TEST_F(FlatlandTest, DefaultHitRegionsExist_OnlyForCurrentRoot) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const auto session_id = flatland->GetSessionId();
const TransformId kId1 = {1};
const TransformHandle handle1 = TransformHandle(session_id, 1);
const TransformId kId2 = {2};
const TransformHandle handle2 = TransformHandle(session_id, 2);
flatland->CreateTransform(kId1);
flatland->CreateTransform(kId2);
flatland->SetRootTransform(kId1);
PRESENT(flatland, true);
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 1u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
EXPECT_EQ(hit_regions[handle2].size(), 0u);
}
flatland->SetRootTransform(kId2);
PRESENT(flatland, true);
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 1u);
EXPECT_EQ(hit_regions[handle1].size(), 0u);
EXPECT_EQ(hit_regions[handle2].size(), 1u);
}
}
TEST_F(FlatlandTest, SetHitRegionsOverwritesPreviousOnes) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const auto session_id = flatland->GetSessionId();
const TransformId kId1 = {1};
const TransformHandle handle1 = TransformHandle(session_id, 1);
flatland->CreateTransform(kId1);
flatland->SetRootTransform(kId1);
PRESENT(flatland, true);
// Check that the default hit region is as expected.
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 1u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
auto hit_region = hit_regions[handle1][0];
EXPECT_FALSE(hit_region.is_finite());
EXPECT_EQ(hit_region.interaction(), fuchsia::ui::composition::HitTestInteraction::DEFAULT);
}
// Add a hit region to a different transform - this should not overwrite the default one.
const TransformId kId2 = {2};
const TransformHandle handle2 = TransformHandle(session_id, 2);
flatland->CreateTransform(kId2);
flatland->SetHitRegions(kId2,
{{{0, 1, 2, 3}, fuchsia_ui_composition::HitTestInteraction::kDefault}});
PRESENT(flatland, true);
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 2u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
EXPECT_EQ(hit_regions[handle2].size(), 1u);
{
auto hit_region = hit_regions[handle1][0];
EXPECT_FALSE(hit_region.is_finite());
EXPECT_EQ(hit_region.interaction(), fuchsia::ui::composition::HitTestInteraction::DEFAULT);
}
{
auto hit_region = hit_regions[handle2][0];
ASSERT_TRUE(hit_region.is_finite());
const auto rect = hit_region.region();
const types::RectangleF expected_rect({.x = 0, .y = 1, .width = 2, .height = 3});
EXPECT_EQ(rect, expected_rect);
EXPECT_EQ(hit_region.interaction(), fuchsia::ui::composition::HitTestInteraction::DEFAULT);
}
}
// Overwrite the default hit region.
flatland->SetHitRegions(
kId1, {{{1, 2, 3, 4}, fuchsia_ui_composition::HitTestInteraction::kSemanticallyInvisible}});
PRESENT(flatland, true);
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 2u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
auto hit_region = hit_regions[handle1][0];
ASSERT_TRUE(hit_region.is_finite());
const auto rect = hit_region.region();
const types::RectangleF expected_rect({.x = 1, .y = 2, .width = 3, .height = 4});
EXPECT_EQ(rect, expected_rect);
EXPECT_EQ(hit_region.interaction(),
fuchsia::ui::composition::HitTestInteraction::SEMANTICALLY_INVISIBLE);
}
}
TEST_F(FlatlandTest, SetRootTransformAfterSetHitRegions_DoesNotChangeHitRegion) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const auto session_id = flatland->GetSessionId();
const TransformId kId1 = {1};
const TransformHandle handle1 = TransformHandle(session_id, 1);
flatland->CreateTransform(kId1);
flatland->SetHitRegions(
kId1, {{{0, 1, 2, 3}, fuchsia_ui_composition::HitTestInteraction::kSemanticallyInvisible}});
flatland->SetRootTransform(kId1);
PRESENT(flatland, true);
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 1u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
auto hit_region = hit_regions[handle1][0];
ASSERT_TRUE(hit_region.is_finite());
const auto rect = hit_region.region();
const types::RectangleF expected_rect({.x = 0, .y = 1, .width = 2, .height = 3});
EXPECT_EQ(rect, expected_rect);
EXPECT_EQ(hit_region.interaction(),
fuchsia::ui::composition::HitTestInteraction::SEMANTICALLY_INVISIBLE);
}
TEST_F(FlatlandTest, MultipleTransformsWithHitRegions) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const auto session_id = flatland->GetSessionId();
const TransformId kId1 = {1};
const TransformHandle handle1 = TransformHandle(session_id, 1);
flatland->CreateTransform(kId1);
flatland->SetHitRegions(kId1,
{{{0, 1, 2, 3}, fuchsia_ui_composition::HitTestInteraction::kDefault}});
const TransformId kId2 = {2};
const TransformHandle handle2 = TransformHandle(session_id, 2);
flatland->CreateTransform(kId2);
flatland->SetHitRegions(
kId2, {{{1, 2, 3, 4}, fuchsia_ui_composition::HitTestInteraction::kSemanticallyInvisible}});
PRESENT(flatland, true);
{
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 2u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
EXPECT_EQ(hit_regions[handle2].size(), 1u);
auto hit_region1 = hit_regions[handle1][0];
auto hit_region2 = hit_regions[handle2][0];
ASSERT_TRUE(hit_region1.is_finite());
auto rect1 = hit_region1.region();
ASSERT_TRUE(hit_region2.is_finite());
auto rect2 = hit_region2.region();
const types::RectangleF expected_rect1({.x = 0, .y = 1, .width = 2, .height = 3});
const types::RectangleF expected_rect2({.x = 1, .y = 2, .width = 3, .height = 4});
EXPECT_EQ(rect1, expected_rect1);
EXPECT_EQ(rect2, expected_rect2);
EXPECT_EQ(hit_region1.interaction(), fuchsia::ui::composition::HitTestInteraction::DEFAULT);
EXPECT_EQ(hit_region2.interaction(),
fuchsia::ui::composition::HitTestInteraction::SEMANTICALLY_INVISIBLE);
}
}
TEST_F(FlatlandTest, ManuallyAddedMaximalHitRegionPersists) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
const auto session_id = flatland->GetSessionId();
const TransformId kId1 = {1};
const TransformHandle handle1 = TransformHandle(session_id, 1);
flatland->CreateTransform(kId1);
flatland->SetHitRegions(kId1, {{{FLT_MIN, FLT_MIN, FLT_MAX, FLT_MAX},
fuchsia_ui_composition::HitTestInteraction::kDefault}});
PRESENT(flatland, true);
flatland->SetRootTransform(kId1);
PRESENT(flatland, true);
flatland->SetRootTransform({0});
PRESENT(flatland, true);
auto uber_struct = GetUberStruct(flatland.get());
auto& hit_regions = uber_struct->local_hit_regions_map;
EXPECT_EQ(hit_regions.size(), 1u);
EXPECT_EQ(hit_regions[handle1].size(), 1u);
auto hit_region1 = hit_regions[handle1][0];
ASSERT_TRUE(hit_region1.is_finite());
auto rect = hit_region1.region();
types::RectangleF expected_rect(
{.x = FLT_MIN, .y = FLT_MIN, .width = FLT_MAX, .height = FLT_MAX});
EXPECT_EQ(rect, expected_rect);
}
TEST_F(FlatlandTest, ChildViewWatcherFailsIdCollision) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport(kId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, true);
ViewportCreationToken parent_token2;
ViewCreationToken child_token2;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token2.value(), &child_token2.value()));
flatland->CreateViewport(kId1, std::move(parent_token2), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ClearDelaysLinkDestructionUntilPresent) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
CreateViewport(parent.get(), child.get(), kLinkId1, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end));
// Clearing the parent graph should not unbind the interfaces until Present() is called and the
// acquire fence is signaled.
parent->Clear();
RunLoopUntilIdle();
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end));
PresentArgs args;
args.acquire_fences = utils::CreateEventArray(1);
auto event_copy = utils::CopyEvent(args.acquire_fences[0]);
PRESENT_WITH_ARGS(parent, std::move(args), true);
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end));
// Signal the acquire fence to unbind the links.
event_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_FALSE(ClientEndPeerExists(parent_viewport_watcher_client_end));
// Recreate the Link. The parent graph was cleared so we can reuse the LinkId.
auto [child_view_watcher_client_end2, child_view_watcher_server_end2] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end2, parent_viewport_watcher_server_end2] =
fidl::Endpoints<ParentViewportWatcher>::Create();
CreateViewport(parent.get(), child.get(), kLinkId1, std::move(child_view_watcher_server_end2),
std::move(parent_viewport_watcher_server_end2));
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end2));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
// Clearing the child graph should not unbind the interfaces until Present() is called and the
// acquire fence is signaled.
child->Clear();
RunLoopUntilIdle();
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end2));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
PresentArgs args2;
args2.acquire_fences = utils::CreateEventArray(1);
event_copy = utils::CopyEvent(args2.acquire_fences[0]);
PRESENT_WITH_ARGS(child, std::move(args2), true);
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end2));
EXPECT_TRUE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
// Signal the acquire fence to unbind the links.
event_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end2));
EXPECT_FALSE(ClientEndPeerExists(parent_viewport_watcher_client_end2));
}
// This test doesn't use the helper function to create a link, because it tests intermediate steps
// and timing corner cases.
TEST_F(FlatlandTest, ChildGetsLayoutUpdateWithoutPresenting) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
// Set up a link, but don't call Present() on either instance.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
// Request a layout update.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = std::move(result->info());
});
// Without even presenting, the child is able to get the initial properties from the parent->
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(layout.has_value());
EXPECT_EQ(1u, layout->logical_size()->width());
EXPECT_EQ(2u, layout->logical_size()->height());
}
TEST_F(FlatlandTest, OverwrittenHangingGetsReturnError) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
// Set up a link, but don't call Present() on either instance.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
UpdateLinks(parent->GetRoot());
// First layout request should succeed immediately.
bool layout_updated = false;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout_updated = true;
});
RunLoopUntilIdle();
EXPECT_TRUE(layout_updated);
// Queue overwriting hanging gets.
layout_updated = false;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
if (result.is_ok()) {
layout_updated = true;
}
});
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
if (result.is_ok()) {
layout_updated = true;
}
});
RunLoopUntilIdle();
EXPECT_FALSE(layout_updated);
// Present should fail on child because the client has broken flow control.
PresentArgs args;
args.expected_error = FlatlandError::kBadHangingGet;
PRESENT_WITH_ARGS(child, std::move(args), false);
}
// This test doesn't use the helper function to create a link, because it tests intermediate steps
// and timing corner cases.
TEST_F(FlatlandTest, ConnectedToDisplayParentPresentsBeforeChild) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
// Set up a link and attach it to the parent's root, but don't call Present() on either instance.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const TransformId kTransformId = {1};
parent->CreateTransform(kTransformId);
parent->SetRootTransform(kTransformId);
const ContentId kLinkId = {2};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kTransformId, kLinkId);
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
// Request a status update.
std::optional<ParentViewportStatus> parent_status;
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_status = result->status();
});
// The child begins disconnected from the display.
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(parent_status.has_value());
EXPECT_EQ(*parent_status, ParentViewportStatus::kDisconnectedFromDisplay);
// The ParentViewportStatus will update when both the parent and child Present().
parent_status.reset();
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_status = result->status();
});
// The parent presents first, no update.
PRESENT(parent, true);
UpdateLinks(parent->GetRoot());
EXPECT_FALSE(parent_status.has_value());
// The child presents second and the status updates.
PRESENT(child, true);
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(parent_status.has_value());
EXPECT_EQ(*parent_status, ParentViewportStatus::kConnectedToDisplay);
}
// This test doesn't use the helper function to create a link, because it tests intermediate steps
// and timing corner cases.
TEST_F(FlatlandTest, ConnectedToDisplayChildPresentsBeforeParent) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
// Set up a link and attach it to the parent's root, but don't call Present() on either instance.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const TransformId kTransformId = {1};
parent->CreateTransform(kTransformId);
parent->SetRootTransform(kTransformId);
const ContentId kLinkId = {2};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kTransformId, kLinkId);
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
// Request a status update.
std::optional<ParentViewportStatus> parent_status;
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_status = result->status();
});
// The child begins disconnected from the display.
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(parent_status.has_value());
EXPECT_EQ(*parent_status, ParentViewportStatus::kDisconnectedFromDisplay);
// The ParentViewportStatus will update when both the parent and child Present().
parent_status.reset();
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_status = result->status();
});
// The child presents first, no update.
PRESENT(child, true);
UpdateLinks(parent->GetRoot());
EXPECT_FALSE(parent_status.has_value());
// The parent presents second and the status updates.
PRESENT(parent, true);
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(parent_status.has_value());
EXPECT_EQ(*parent_status, ParentViewportStatus::kConnectedToDisplay);
}
// This test doesn't use the helper function to create a link, because it tests intermediate steps
// and timing corner cases.
TEST_F(FlatlandTest, ChildReceivesDisconnectedFromDisplay) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
// Set up a link and attach it to the parent's root, but don't call Present() on either instance.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const TransformId kTransformId = {1};
parent->CreateTransform(kTransformId);
parent->SetRootTransform(kTransformId);
const ContentId kLinkId = {2};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kTransformId, kLinkId);
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
// The ParentViewportStatus will update when both the parent and child Present().
std::optional<ParentViewportStatus> parent_status;
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_status = result->status();
});
PRESENT(child, true);
PRESENT(parent, true);
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(parent_status.has_value());
EXPECT_EQ(*parent_status, ParentViewportStatus::kConnectedToDisplay);
// The ParentViewportStatus will update again if the parent removes the child link from its
// topology.
parent_status.reset();
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_status = result->status();
});
parent->SetContent(kTransformId, {0});
PRESENT(parent, true);
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(parent_status.has_value());
EXPECT_EQ(*parent_status, ParentViewportStatus::kDisconnectedFromDisplay);
}
// This test doesn't use the helper function to create a link, because it tests intermediate steps
// and timing corner cases.
TEST_F(FlatlandTest, ValidChildToParentFlow_ChildUsedCreateView2) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_viewport_token;
ViewCreationToken child_view_token;
ASSERT_EQ(ZX_OK,
zx::channel::create(0, &parent_viewport_token.value(), &child_view_token.value()));
const ContentId kLinkId = {1};
const TransformId kRootTransform = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
fidl::Client child_view_watcher(std::move(child_view_watcher_client_end), dispatcher());
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_viewport_token), std::move(properties),
std::move(child_view_watcher_server_end));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_view_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
std::optional<ChildViewStatus> child_status;
child_view_watcher->GetStatus().ThenExactlyOnce([&](ChildViewWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
ASSERT_EQ(ChildViewStatus::kContentHasPresented, result->status());
child_status = result->status();
});
std::optional<fuchsia_ui_views::ViewRef> child_viewref;
child_view_watcher->GetViewRef().ThenExactlyOnce([&](ChildViewWatcher_GetViewRefResult& result) {
ASSERT_TRUE(result.is_ok());
child_viewref = std::move(result->view_ref());
});
// ChildViewStatus changes as soon as the child presents. The parent does not have to present.
EXPECT_FALSE(child_status.has_value());
PRESENT(child, true);
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(child_status.has_value());
// Note that although CONTENT_HAS_PRESENTED is signaled, GetViewRef() does not yet return the ref.
// This is because although the parent and child are connected, neither appears in the global
// topology, because neither is connected to the root.
EXPECT_FALSE(child_viewref.has_value());
// Having the parent present is still not sufficient to fulfill GetViewRef(), because the viewport
// has not been added to the parent's local sub-tree, and therefore doesn't appear in the global
// topology.
PRESENT(parent, true);
UpdateLinks(parent->GetRoot());
EXPECT_FALSE(child_viewref.has_value());
// Adding the viewport to the parent's local tree, then presenting, is sufficient for the call to
// GetViewRef() to succeed.
parent->CreateTransform(kRootTransform);
parent->SetRootTransform(kRootTransform);
parent->SetContent(kRootTransform, kLinkId);
// While we're at it, add an acquire fence and make sure that the ViewRef isn't sent until the
// event is signaled.
PresentArgs args;
args.acquire_fences = utils::CreateEventArray(1);
auto event_copy = utils::CopyEvent(args.acquire_fences[0]);
// We still don't get the ViewRef, because we haven't signaled the event.
PRESENT_WITH_ARGS(parent, std::move(args), true);
UpdateLinks(parent->GetRoot());
EXPECT_FALSE(child_viewref.has_value());
// Signal the acquire fence to unblock the present.
event_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(child_viewref.has_value());
EXPECT_TRUE(child_viewref->reference());
}
TEST_F(FlatlandTest, ValidChildToParentFlow_ChildUsedCreateView) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_viewport_token;
ViewCreationToken child_view_token;
ASSERT_EQ(ZX_OK,
zx::channel::create(0, &parent_viewport_token.value(), &child_view_token.value()));
const ContentId kLinkId = {1};
const TransformId kRootTransform = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
fidl::Client child_view_watcher(std::move(child_view_watcher_client_end), dispatcher());
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_viewport_token), std::move(properties),
std::move(child_view_watcher_server_end));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView(std::move(child_view_token), std::move(parent_viewport_watcher_server_end));
std::optional<ChildViewStatus> child_status;
child_view_watcher->GetStatus().ThenExactlyOnce([&](ChildViewWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
child_status = result->status();
});
// ChildViewStatus changes as soon as the child presents. The parent does not have to present.
EXPECT_FALSE(child_status.has_value());
PRESENT(child, true);
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(child_status.has_value());
ASSERT_EQ(ChildViewStatus::kContentHasPresented, *child_status);
}
TEST_F(FlatlandTest, ContentHasPresentedSignalWaitsForAcquireFences) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
fidl::Client child_view_watcher(std::move(child_view_watcher_client_end), dispatcher());
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{1, 2});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
std::optional<ChildViewStatus> cvs;
child_view_watcher->GetStatus().ThenExactlyOnce([&](ChildViewWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
ASSERT_EQ(ChildViewStatus::kContentHasPresented, result->status());
cvs = result->status();
});
// ChildViewStatus changes as soon as the child presents. The parent does not have to present.
EXPECT_FALSE(cvs.has_value());
// Present the child with unsignalled acquire fence.
PresentArgs args;
args.acquire_fences = utils::CreateEventArray(1);
auto acquire1_copy = utils::CopyEvent(args.acquire_fences[0]);
EXPECT_FALSE(utils::IsEventSignalled(args.acquire_fences[0], ZX_EVENT_SIGNALED));
PRESENT_WITH_ARGS(child, std::move(args), true);
// Hanging get should not be run yet because the fence is not signalled.
UpdateLinks(parent->GetRoot());
EXPECT_FALSE(cvs.has_value());
// Signal the acquire fence.
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
acquire1_copy.signal(0, ZX_EVENT_SIGNALED);
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
// Hanging get should run now.
UpdateLinks(parent->GetRoot());
EXPECT_TRUE(cvs.has_value());
EXPECT_EQ(cvs.value(), ChildViewStatus::kContentHasPresented);
}
TEST_F(FlatlandTest, SetViewportProperties_WithDeadViewport_ShouldNotCrash) {
std::shared_ptr<Flatland> parent = CreateFlatland();
const TransformId kTransformId = {1};
const ContentId kLinkId = {2};
ViewportCreationToken parent_token;
{
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Child token goes out of scope.
}
{
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(parent, true);
}
{
// Now call SetViewportProperties() and make sure we don't crash.
parent->SetViewportProperties(kLinkId, ViewportProperties{});
PRESENT(parent, true);
}
}
TEST_F(FlatlandTest, CreateViewport_CorrectlySetsPropertiesDefaults) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId1 = {2};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
CreateViewport(parent.get(), child.get(), kLinkId1, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
{
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
ASSERT_TRUE(layout->inset().has_value());
EXPECT_EQ(layout->logical_size()->width(), kDefaultSize);
EXPECT_EQ(layout->logical_size()->height(), kDefaultSize);
EXPECT_EQ(layout->inset()->top(), kDefaultInset);
EXPECT_EQ(layout->inset()->right(), kDefaultInset);
EXPECT_EQ(layout->inset()->bottom(), kDefaultInset);
EXPECT_EQ(layout->inset()->left(), kDefaultInset);
}
}
TEST_F(FlatlandTest, AfterSetViewportProperties_NewLayoutIsDeliveredWithoutPresenting) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId = {2};
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
{ // Create Viewport and child View.
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
CreateViewport(parent.get(), child.get(), kLinkId, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
}
// Get and throw away initial layout received.
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[](ParentViewportWatcher_GetLayoutResult& result) {});
RunLoopUntilIdle();
{ // Call SetViewportProperties(), but don't present.
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize + 1, kDefaultSize + 2});
properties.inset(fuchsia_math::Inset{}.top(4).right(5).bottom(6).left(7));
parent->SetViewportProperties(kLinkId, std::move(properties));
}
{ // Observe new layout being delivered immediately.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
ASSERT_TRUE(layout->inset().has_value());
EXPECT_EQ(layout->logical_size()->width(), kDefaultSize + 1);
EXPECT_EQ(layout->logical_size()->height(), kDefaultSize + 2);
EXPECT_EQ(layout->inset()->top(), 4);
EXPECT_EQ(layout->inset()->right(), 5);
EXPECT_EQ(layout->inset()->bottom(), 6);
EXPECT_EQ(layout->inset()->left(), 7);
}
}
TEST_F(FlatlandTest, SetViewportProperties_BeforeLinkResolution_ShouldUpdateInitialLayout) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId1 = {2};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
{ // Create the Viewport.
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(kLinkId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(parent, true);
}
{ // Before the child's View is created, call SetViewportProperties.
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize + 1, kDefaultSize + 2});
properties.inset(fuchsia_math::Inset{}.top(4).right(5).bottom(6).left(7));
parent->SetViewportProperties(kLinkId1, std::move(properties));
}
{ // Create the child View to let the link resolve.
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
PRESENT(child, true);
}
{ // Observe that the initial layout received was updated in the SetViewProperties() call.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
ASSERT_TRUE(layout->inset().has_value());
EXPECT_EQ(layout->logical_size()->width(), kDefaultSize + 1);
EXPECT_EQ(layout->logical_size()->height(), kDefaultSize + 2);
EXPECT_EQ(layout->inset()->top(), 4);
EXPECT_EQ(layout->inset()->right(), 5);
EXPECT_EQ(layout->inset()->bottom(), 6);
EXPECT_EQ(layout->inset()->left(), 7);
}
}
TEST_F(FlatlandTest, SetViewportProperties_HandlesMissingValues) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId = {2};
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
{
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
CreateViewport(parent.get(), child.get(), kLinkId, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
}
{ // SetViewportProperties with only the logical size set.
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{2, 3});
parent->SetViewportProperties(kLinkId, std::move(properties));
}
{ // Confirm that the logical size is updated and the rest is unchanged.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
ASSERT_TRUE(layout->inset().has_value());
EXPECT_EQ(layout->logical_size()->width(), 2u);
EXPECT_EQ(layout->logical_size()->height(), 3u);
EXPECT_EQ(layout->inset()->top(), kDefaultInset);
EXPECT_EQ(layout->inset()->left(), kDefaultInset);
EXPECT_EQ(layout->inset()->bottom(), kDefaultInset);
EXPECT_EQ(layout->inset()->right(), kDefaultInset);
}
{ // Set the inset to something new.
ViewportProperties properties;
properties.inset(fuchsia_math::Inset{}.top(4).right(5).bottom(6).left(7));
parent->SetViewportProperties(kLinkId, std::move(properties));
}
{ // Confirm that the insets are updated and the rest is unchanged.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
ASSERT_TRUE(layout->inset().has_value());
EXPECT_EQ(layout->logical_size()->width(), 2u);
EXPECT_EQ(layout->logical_size()->height(), 3u);
EXPECT_EQ(layout->inset()->top(), 4);
EXPECT_EQ(layout->inset()->right(), 5);
EXPECT_EQ(layout->inset()->bottom(), 6);
EXPECT_EQ(layout->inset()->left(), 7);
}
{ // Call SetViewportProperties with an empty table.
ViewportProperties empty_properties;
parent->SetViewportProperties(kLinkId, std::move(empty_properties));
}
{ // Confirm that no update has been triggered.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
if (result.is_ok()) {
layout = result->info();
}
});
RunLoopUntilIdle();
EXPECT_FALSE(layout.has_value());
}
}
TEST_F(FlatlandTest,
SetViewportProperties_MultipleTimes_ShouldOnlyDeliverTheNewestValue_OnHangingGet) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId = {2};
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
{
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
CreateViewport(parent.get(), child.get(), kLinkId, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
}
const uint32_t kInitialSize = 100;
// Set the logical size to something new multiple times.
for (int i = 10; i >= 0; --i) {
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kInitialSize + i + 1, kInitialSize + i + 1});
parent->SetViewportProperties(kLinkId, std::move(properties));
ViewportProperties properties2;
properties2.logical_size(fuchsia_math::SizeU{kInitialSize + i, kInitialSize + i});
parent->SetViewportProperties(kLinkId, std::move(properties2));
}
{ // Confirm that the callback fires, and that it has the most up-to-date data.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
EXPECT_EQ(layout->logical_size()->width(), kInitialSize);
EXPECT_EQ(layout->logical_size()->height(), kInitialSize);
}
{ // Confirm that calling GetLayout again results in a hung get.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_FALSE(layout.has_value());
// Update the properties again and observe the pending GetLayout() triggering.
const uint32_t kNewSize = 50u;
{
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kNewSize, kNewSize});
parent->SetViewportProperties(kLinkId, std::move(properties));
}
RunLoopUntilIdle();
// Confirm that we receive the update and that it had the newest values.
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
EXPECT_EQ(layout->logical_size()->width(), kNewSize);
EXPECT_EQ(layout->logical_size()->height(), kNewSize);
}
}
TEST_F(FlatlandTest, SetViewportProperties_OnMultipleChildren_ShouldUpdateEachOne) {
const int kNumChildren = 3;
const ContentId kLinkIds[kNumChildren] = {{5}, {6}, {7}};
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> children[kNumChildren] = {CreateFlatland(), CreateFlatland(),
CreateFlatland()};
fidl::Client<ParentViewportWatcher> parent_viewport_watcher[kNumChildren];
for (int i = 0; i < kNumChildren; ++i) {
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
CreateViewport(parent.get(), children[i].get(), kLinkIds[i],
std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
parent_viewport_watcher[i].Bind(std::move(parent_viewport_watcher_client_end), dispatcher());
}
// Confirm that all children are at the default value
for (int i = 0; i < kNumChildren; ++i) {
std::optional<LayoutInfo> layout;
parent_viewport_watcher[i]->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
ASSERT_TRUE(result->info().logical_size().has_value());
EXPECT_EQ(kDefaultSize, result->info().logical_size()->width());
EXPECT_EQ(kDefaultSize, result->info().logical_size()->height());
layout = result->info();
});
RunLoopUntilIdle();
}
// Resize the content on all children.
for (auto id : kLinkIds) {
SizeU size;
size.width(id.value());
size.height(id.value() * 2);
ViewportProperties properties;
properties.logical_size(size);
parent->SetViewportProperties(id, std::move(properties));
}
// Confirm that all children are updated.
for (int i = 0; i < kNumChildren; ++i) {
std::optional<LayoutInfo> layout;
parent_viewport_watcher[i]->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->logical_size().has_value());
EXPECT_EQ(kLinkIds[i].value(), layout->logical_size()->width());
EXPECT_EQ(kLinkIds[i].value() * 2, layout->logical_size()->height());
}
}
TEST_F(FlatlandTest, LinkSystem_WhenSettingDevicePixelRatio_ItShouldBeTransmittedToLayoutInfo) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId = {2};
const glm::vec2 initial_dpr = {3.f, 4.f};
link_system_->UpdateDevicePixelRatio(initial_dpr);
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
{
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
fidl::Client child_view_watcher(std::move(child_view_watcher_client_end), dispatcher());
CreateViewport(parent.get(), child.get(), kLinkId, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
}
{ // Observe the DPR in the initial received layout.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->device_pixel_ratio().has_value());
EXPECT_EQ(layout->device_pixel_ratio()->x(), initial_dpr.x);
EXPECT_EQ(layout->device_pixel_ratio()->y(), initial_dpr.y);
}
// Set a new DPR.
const glm::vec2 new_dpr = {5.f, 6.f};
link_system_->UpdateDevicePixelRatio(new_dpr);
{ // Observe the new DPR being delivered.
std::optional<LayoutInfo> layout;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout = result->info();
});
RunLoopUntilIdle();
ASSERT_TRUE(layout.has_value());
ASSERT_TRUE(layout->device_pixel_ratio().has_value());
EXPECT_EQ(layout->device_pixel_ratio()->x(), new_dpr.x);
EXPECT_EQ(layout->device_pixel_ratio()->y(), new_dpr.y);
}
}
TEST_F(FlatlandTest, SetLinkOnTransformErrorCases) {
const TransformId kId1 = {1};
const TransformId kId2 = {2};
const ContentId kLinkId1 = {1};
const ContentId kLinkId2 = {2};
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Creating a link with an empty property object is an error. Logical size must be provided at
// creation time.
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
ViewportProperties empty_properties;
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport(kLinkId1, std::move(parent_token), std::move(empty_properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, false);
}
// Setup.
auto SetupFlatland = [&]() {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
zx::channel::create(0, &parent_token.value(), &child_token.value());
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport(kLinkId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
return flatland;
};
// Zero is not a valid transform_id.
{
auto flatland = SetupFlatland();
PRESENT(flatland, true);
flatland->SetContent({0}, kLinkId1);
PRESENT(flatland, false);
}
// Setting a valid link on an invalid transform is not valid.
{
auto flatland = SetupFlatland();
PRESENT(flatland, true);
flatland->SetContent(kId2, kLinkId1);
PRESENT(flatland, false);
}
// Setting an invalid link on a valid transform is not valid.
{
auto flatland = SetupFlatland();
flatland->CreateTransform(kId1);
PRESENT(flatland, true);
flatland->SetContent(kId1, kLinkId2);
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, ReleaseViewportErrorCases) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
// Zero is not a valid link_id.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseViewport({0}, [](ViewportCreationToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
}
// Using a link_id that does not exist is not valid.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
const ContentId kLinkId1 = {1};
flatland->ReleaseViewport(kLinkId1, [](ViewportCreationToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
}
// ContentId is not a Link.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
const ContentId kImageId = {2};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{100, 200});
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties));
flatland->ReleaseViewport(kImageId, [](ViewportCreationToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
}
}
// In most of the tests, we invoke methods directly on a shared_ptr<Flatland> rather than over a
// FIDL channel. In most cases, this exercises the code sufficiently. However, `ReleaseViewport()`
// is different because has a return value, which means that its `fidl::Completer` will close the
// channel if it is not completed.
TEST_F(FlatlandTest, ReleaseViewportViaFidlClient) {
const ContentId kViewportId = {1};
// Create a viewport. Returns the ChildViewWatcher client-end, to allow it to be kept alive.
auto CreateViewport =
[&](FlatlandEventLoopClientServer& client_server,
ContentId viewport_id) -> fidl::ClientEnd<fuchsia_ui_composition::ChildViewWatcher> {
ViewportCreationToken parent_token;
ViewCreationToken child_token;
EXPECT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto child_view_watcher_endpoints = fidl::Endpoints<ChildViewWatcher>::Create();
libsync::Completion completion;
async::PostTask(client_server.server_loop().dispatcher(), [&]() {
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
client_server.server()->CreateViewport(viewport_id, std::move(parent_token),
std::move(properties),
std::move(child_view_watcher_endpoints.server));
PRESENT(client_server.server(), true);
completion.Signal();
});
completion.Wait();
return std::move(child_view_watcher_endpoints.client);
};
{
auto client_server = CreateFlatlandEventLoopClientServer();
auto child_view_watcher_client_end = CreateViewport(*client_server, kViewportId);
// Illegally invoke `ReleaseViewport()` and wait for the response, which requires a server
// round-trip.
libsync::Completion completion;
async::PostTask(client_server->client_loop().dispatcher(), [&]() {
const ContentId kInvalidViewportId = {0};
(*client_server->client())
->ReleaseViewport(kInvalidViewportId)
.ThenExactlyOnce(
[&](fidl::Result<fuchsia_ui_composition::Flatland::ReleaseViewport>& result) {
ASSERT_TRUE(result.is_error());
completion.Signal();
});
});
completion.Wait();
}
{
auto client_server = CreateFlatlandEventLoopClientServer();
auto child_view_watcher_client_end = CreateViewport(*client_server, kViewportId);
// Illegally invoke `ReleaseViewport()` and wait for the response, which requires a server
// round-trip.
libsync::Completion completion;
async::PostTask(client_server->client_loop().dispatcher(), [&]() {
const ContentId kNonExistentViewportId = {420};
(*client_server->client())
->ReleaseViewport(kNonExistentViewportId)
.ThenExactlyOnce(
[&](fidl::Result<fuchsia_ui_composition::Flatland::ReleaseViewport>& result) {
ASSERT_TRUE(result.is_error());
completion.Signal();
});
});
completion.Wait();
}
{
auto client_server = CreateFlatlandEventLoopClientServer();
auto child_view_watcher_client_end = CreateViewport(*client_server, kViewportId);
// Release the viewport. This will not complete on the server.
libsync::Completion completion;
async::PostTask(client_server->client_loop().dispatcher(), [&]() {
(*client_server->client())
->ReleaseViewport(kViewportId)
.ThenExactlyOnce(
[&](fidl::Result<fuchsia_ui_composition::Flatland::ReleaseViewport>& result) {
ASSERT_TRUE(result.is_ok());
completion.Signal();
});
});
// We don't want to drastically increase the time takes to run, so wait only 100ms.
// We "know" this would timeout no matter how long we wait, but
zx_status_t result = completion.Wait(zx::duration(100'000'000));
EXPECT_EQ(result, ZX_ERR_TIMED_OUT);
// Now present. We do this via the FIDL client to ensure correct sequencing (i.e. this way the
// `Present()` is guaranteed to follow the `ReleaseViewport()`).
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
async::PostTask(client_server->client_loop().dispatcher(), [&]() {
fuchsia_ui_composition::PresentArgs present_args;
present_args.requested_presentation_time(0)
.acquire_fences({})
.release_fences({})
.unsquashable(true);
auto result = (*client_server->client())->Present(std::move(present_args));
ASSERT_TRUE(result.is_ok());
});
completion.Wait();
}
}
TEST_F(FlatlandTest, ReleaseViewportReturnsOriginalToken) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const zx_koid_t expected_koid = fsl::GetKoid(parent_token.value().get());
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport(kLinkId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, true);
ViewportCreationToken content_token;
flatland->ReleaseViewport(kLinkId1, [&content_token](ViewportCreationToken token) {
content_token = std::move(token);
});
RunLoopUntilIdle();
// Until Present() is called and the acquire fence is signaled, the previous ChildViewWatcher is
// not unbound.
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_FALSE(content_token.value().is_valid());
PresentArgs args;
args.acquire_fences = utils::CreateEventArray(1);
auto event_copy = utils::CopyEvent(args.acquire_fences[0]);
PRESENT_WITH_ARGS(flatland, std::move(args), true);
EXPECT_TRUE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_FALSE(content_token.value().is_valid());
// Signal the acquire fence to unbind the link.
event_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
RunLoopUntilIdle();
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end));
EXPECT_TRUE(content_token.value().is_valid());
EXPECT_EQ(fsl::GetKoid(content_token.value().get()), expected_koid);
}
TEST_F(FlatlandTest, ReleaseViewportReturnsOrphanedTokenOnChildDeath) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport(kLinkId1, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, true);
// Killing the peer token does not prevent the instance from returning a valid token.
child_token.value().reset();
RunLoopUntilIdle();
ViewportCreationToken content_token;
flatland->ReleaseViewport(kLinkId1, [&content_token](ViewportCreationToken token) {
content_token = std::move(token);
});
PRESENT(flatland, true);
EXPECT_TRUE(content_token.value().is_valid());
// But trying to link with that token will immediately fail because it is already orphaned.
const ContentId kLinkId2 = {2};
auto [child_view_watcher_client_end2, child_view_watcher_server_end2] =
fidl::Endpoints<ChildViewWatcher>::Create();
flatland->CreateViewport(kLinkId2, std::move(content_token), std::move(properties),
std::move(child_view_watcher_server_end2));
PRESENT(flatland, true);
EXPECT_FALSE(ClientEndPeerExists(child_view_watcher_client_end2));
}
TEST_F(FlatlandTest, CreateViewportPresentedBeforeCreateView) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Create a transform, add it to the parent, then create a link and assign to the transform.
const TransformId kId1 = {1};
parent->CreateTransform(kId1);
parent->SetRootTransform(kId1);
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kId1, kLinkId);
PRESENT(parent, true);
// Link the child to the parent->
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
// The child should only be accessible from the parent when Present() is called on the child->
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
PRESENT(child, true);
EXPECT_TRUE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
}
TEST_F(FlatlandTest, CreateViewPresentedBeforeCreateViewport) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Link the child to the parent
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
PRESENT(child, true);
// Create a transform, add it to the parent, then create a link and assign to the transform.
const TransformId kId1 = {1};
parent->CreateTransform(kId1);
parent->SetRootTransform(kId1);
// Present the parent once so that it has a topology or else IsDescendantOf() will crash.
PRESENT(parent, true);
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kId1, kLinkId);
// The child should only be accessible from the parent when Present() is called on the parent->
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
PRESENT(parent, true);
EXPECT_TRUE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
}
TEST_F(FlatlandTest, LinkResolvedBeforeEitherPresent) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Create a transform, add it to the parent, then create a link and assign to the transform.
const TransformId kId1 = {1};
parent->CreateTransform(kId1);
parent->SetRootTransform(kId1);
// Present the parent once so that it has a topology or else IsDescendantOf() will crash.
PRESENT(parent, true);
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kId1, kLinkId);
// Link the child to the parent->
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
// The child should only be accessible from the parent when Present() is called on both the
// parent and the child->
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
PRESENT(parent, true);
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
PRESENT(child, true);
EXPECT_TRUE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
}
TEST_F(FlatlandTest, ClearLinkToChild) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
// Create and link the two instances.
const TransformId kId1 = {1};
parent->CreateTransform(kId1);
parent->SetRootTransform(kId1);
const ContentId kLinkId = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
parent->SetContent(kId1, kLinkId);
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
child->CreateView2(std::move(child_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()), NoViewProtocols(),
std::move(parent_viewport_watcher_server_end));
PRESENT(parent, true);
PRESENT(child, true);
EXPECT_TRUE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
// Reset the child link using zero as the link id.
parent->SetContent(kId1, {0});
PRESENT(parent, true);
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
}
TEST_F(FlatlandTest, RecreateReleasedLinkSameToken) {
std::shared_ptr<Flatland> parent = CreateFlatland();
std::shared_ptr<Flatland> child = CreateFlatland();
// Create link and Present.
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
zx::unowned_channel unowned_child_view_watcher_client_end(
child_view_watcher_client_end.channel().get());
fidl::Client child_view_watcher(std::move(child_view_watcher_client_end), dispatcher());
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
zx::unowned_channel unowned_parent_viewport_watcher_client_end(
parent_viewport_watcher_client_end.channel().get());
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
CreateViewport(parent.get(), child.get(), kLinkId1, std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
RunLoopUntilIdle();
const TransformId kId1 = {1};
parent->CreateTransform(kId1);
parent->SetRootTransform(kId1);
parent->SetContent(kId1, kLinkId1);
PRESENT(parent, true);
EXPECT_TRUE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
// Both protocols should be bound at this point.
EXPECT_TRUE(PeerExists(unowned_child_view_watcher_client_end));
bool child_view_watcher_updated = false;
child_view_watcher->GetStatus().ThenExactlyOnce(
[&child_view_watcher_updated](ChildViewWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
child_view_watcher_updated = true;
});
EXPECT_FALSE(child_view_watcher_updated);
UpdateLinks(parent->GetRoot());
RunLoopUntilIdle();
EXPECT_TRUE(child_view_watcher_updated);
EXPECT_TRUE(PeerExists(unowned_parent_viewport_watcher_client_end));
// Release the link on parent.
ViewportCreationToken content_token;
parent->ReleaseViewport(kLinkId1, [&content_token](ViewportCreationToken token) {
content_token = std::move(token);
});
PRESENT(parent, true);
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
// The same token can be used to create a different link to the same child with a different
// parent.
std::shared_ptr<Flatland> parent2 = CreateFlatland();
const TransformId kId2 = {2};
parent2->CreateTransform(kId2);
parent2->SetRootTransform(kId2);
auto [child_view_watcher_client_end2, child_view_watcher_server_end2] =
fidl::Endpoints<ChildViewWatcher>::Create();
zx::unowned_channel unowned_child_view_watcher_client_end2(
child_view_watcher_client_end2.channel().get());
fidl::Client child_view_watcher2(std::move(child_view_watcher_client_end2), dispatcher());
const ContentId kLinkId2 = {2};
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent2->CreateViewport(kLinkId2, std::move(content_token), std::move(properties),
std::move(child_view_watcher_server_end2));
parent2->SetContent(kId2, kLinkId2);
PRESENT(parent2, true);
EXPECT_TRUE(IsDescendantOf(parent2->GetRoot(), child->GetRoot()));
// The old instance is not re-linked.
EXPECT_FALSE(IsDescendantOf(parent->GetRoot(), child->GetRoot()));
// Both protocols should still be bound.
EXPECT_TRUE(PeerExists(unowned_child_view_watcher_client_end2));
bool child_view_watcher_updated2 = false;
child_view_watcher2->GetStatus().ThenExactlyOnce(
[&child_view_watcher_updated2](ChildViewWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
child_view_watcher_updated2 = true;
});
EXPECT_FALSE(child_view_watcher_updated2);
UpdateLinks(parent->GetRoot());
RunLoopUntilIdle();
EXPECT_TRUE(child_view_watcher_updated2);
EXPECT_TRUE(PeerExists(unowned_parent_viewport_watcher_client_end));
bool parent_viewport_watcher_updated = false;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
parent_viewport_watcher_updated = true;
});
EXPECT_FALSE(parent_viewport_watcher_updated);
RunLoopUntilIdle();
EXPECT_TRUE(parent_viewport_watcher_updated);
}
TEST_F(FlatlandTest, CreateImageValidCase) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{100, 200});
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties));
}
TEST_F(FlatlandTest, CreateImageSetsDefaults) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
const uint32_t kWidth = 100;
const uint32_t kHeight = 200;
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{kWidth, kHeight});
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties));
const auto maybe_image_handle = flatland->GetContentHandle(kImageId);
ASSERT_TRUE(maybe_image_handle.has_value());
const auto image_handle = maybe_image_handle.value();
auto uber_struct = GetUberStruct(flatland.get());
// Default sample region should be same as size.
auto sample_region_kv = uber_struct->local_image_sample_regions.find(image_handle);
EXPECT_NE(sample_region_kv, uber_struct->local_image_sample_regions.end());
fuchsia::math::RectF rect = {0, 0, kWidth, kHeight};
EXPECT_EQ(sample_region_kv->second,
types::RectangleF({.x = 0, .y = 0, .width = kWidth, .height = kHeight}));
// Default destination rect should be same as size.
auto matrix_kv = uber_struct->local_matrices.find(image_handle);
EXPECT_NE(matrix_kv, uber_struct->local_matrices.end());
EXPECT_EQ(sample_region_kv->second.width(), static_cast<float>(kWidth));
EXPECT_EQ(sample_region_kv->second.height(), static_cast<float>(kHeight));
}
TEST_F(FlatlandTest, SetImageOpacityTestCases) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
const TransformId kTransformId = {3};
const ContentId kId = {1};
const ContentId kIdChild = {2};
// Zero is not a valid content ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageOpacity({0}, 0.5);
PRESENT(flatland, false);
}
// The content id hasn't been imported yet.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageOpacity(kId, 0.5);
PRESENT(flatland, false);
}
// Trying to set opacity on a solid color.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateFilledRect(kId);
flatland->SetImageOpacity(kId, 0.5);
PRESENT(flatland, false);
}
// The alpha values are out of range.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kId, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kId);
flatland->SetImageOpacity(kId, -0.5);
PRESENT(flatland, false);
}
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kId, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kId);
flatland->SetImageOpacity(kId, 1.5);
PRESENT(flatland, false);
}
// Testing now with good values should finally work.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kId, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kId);
flatland->SetImageOpacity(kId, 0.7f);
PRESENT(flatland, true);
}
}
TEST_F(FlatlandTest, SetTransformOpacityTestCases) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
const TransformId kId = {1};
const TransformId kIdChild = {2};
// Zero is not a valid transform ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetOpacity({0}, 0.5);
PRESENT(flatland, false);
}
// The transform id hasn't been imported yet.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetOpacity(kId, 0.5);
PRESENT(flatland, false);
}
// The alpha values are out of range.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid transform.
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetOpacity(kId, -0.5);
PRESENT(flatland, false);
}
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid transform.
flatland->CreateTransform(kId);
flatland->SetRootTransform(kId);
flatland->SetOpacity(kId, 1.5);
PRESENT(flatland, false);
}
// Testing now with good values should finally work.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid transform.
flatland->CreateTransform(kId);
PRESENT(flatland, true);
flatland->SetRootTransform(kId);
flatland->SetOpacity(kId, 0.5);
PRESENT(flatland, true);
}
}
TEST_F(FlatlandTest, CreateFilledRectErrorTest) {
const ContentId kInvalidId = {0};
// Zero is not a valid content ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateFilledRect(kInvalidId);
PRESENT(flatland, false);
}
// Same ID can't be imported twice.
{
const ContentId kId = {1};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateFilledRect(kId);
PRESENT(flatland, true);
flatland->CreateFilledRect(kId);
PRESENT(flatland, false);
}
// Test SetSolidFill function.
{
const ContentId kId2 = {2};
// Can't call SetSolidFill on invalid ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetSolidFill(kInvalidId, {1, 0, 0, 1}, {20, 30});
PRESENT(flatland, false);
}
// Can't call SetSolidFill on ID that hasn't been created.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetSolidFill(kId2, {1, 0, 0, 1}, {20, 30});
PRESENT(flatland, false);
}
// Now it should work after creating the filled rect first.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateFilledRect(kId2);
flatland->SetSolidFill(kId2, {1, 0, 0, 1}, {20, 30});
PRESENT(flatland, true);
}
// Try various values and make sure present still returns true.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateFilledRect(kId2);
flatland->SetSolidFill(kId2, {0.7f, 0.3f, 0.9f, 0.4f}, {20, 30});
PRESENT(flatland, true);
}
}
// Test ReleaseFilledRect function
{
const ContentId kId3 = {3};
// Cannot release an invalid ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseFilledRect(kInvalidId);
PRESENT(flatland, false);
}
// Cannot release an ID that hasn't been created.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseFilledRect(kId3);
PRESENT(flatland, false);
}
// Now it should work once we create it first.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateFilledRect(kId3);
flatland->ReleaseFilledRect(kId3);
PRESENT(flatland, true);
// And now we should be able to reuse the same id.
flatland->CreateFilledRect(kId3);
PRESENT(flatland, true);
}
}
}
// Make sure that the data for filled rects gets passed along
// correctly to the uberstructs.
TEST_F(FlatlandTest, FilledRectUberstructTest) {
const ContentId kFilledRectId = {1};
const ContentId kChildRectId = {3};
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Create constants.
const uint32_t kFilledWidth = 50;
const uint32_t kFilledHeight = 100;
const uint32_t kFilledChildWidth = 75;
const uint32_t kFilledChildHeight = 220;
// Create a filled rect and set its color to magenta with a size
// of (50, 100);
fuchsia_ui_composition::ColorRgba rect_color = {0.75, 0.5, 0.25, 1.0};
flatland->CreateFilledRect(kFilledRectId);
flatland->SetSolidFill(kFilledRectId, rect_color, {kFilledWidth, kFilledHeight});
PRESENT(flatland, true);
// Create a second filled rect, set its color to blue, with a size of 75, 220;
fuchsia_ui_composition::ColorRgba child_color = {0.50, 0.75, 1.0, 0.25};
flatland->CreateFilledRect(kChildRectId);
flatland->SetSolidFill(kChildRectId, child_color, {kFilledChildWidth, kFilledChildHeight});
PRESENT(flatland, true);
// Create the transform graph. We will have a root node with one rectangle as content,
// a child node, and a second rectangle as content on the child node.
const TransformId kTransformId = {2};
const TransformId kChildTransformId = {4};
// Create both transforms.
flatland->CreateTransform(kTransformId);
flatland->CreateTransform(kChildTransformId);
// Set the root of the tree, and set the content on that root.
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kFilledRectId);
// Add the child transform to the parent transform, and set the child rectangle as
// content on the child transform.
flatland->AddChild(kTransformId, kChildTransformId);
flatland->SetContent(kChildTransformId, kChildRectId);
PRESENT(flatland, true);
// Get the filled rect content handle.
const auto maybe_rect_handle = flatland->GetContentHandle(kFilledRectId);
ASSERT_TRUE(maybe_rect_handle.has_value());
const auto rect_handle = maybe_rect_handle.value();
// Get the filled child rect handle.
const auto maybe_child_rect_handle = flatland->GetContentHandle(kChildRectId);
ASSERT_TRUE(maybe_child_rect_handle.has_value());
const auto child_rect_handle = maybe_child_rect_handle.value();
// Now find the data for both rectangles in the uber struct. The last handle
// should be that of the child rectangle.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, child_rect_handle);
// Grab the metadata for each handle.
auto image_kv = uber_struct->images.find(rect_handle);
EXPECT_NE(image_kv, uber_struct->images.end());
auto child_image_kv = uber_struct->images.find(child_rect_handle);
EXPECT_NE(child_image_kv, uber_struct->images.end());
// Make sure the color for each rectangle matches the above colors.
EXPECT_EQ(image_kv->second.multiply_color[0], rect_color.red());
EXPECT_EQ(image_kv->second.multiply_color[1], rect_color.green());
EXPECT_EQ(image_kv->second.multiply_color[2], rect_color.blue());
EXPECT_EQ(image_kv->second.multiply_color[3], rect_color.alpha());
EXPECT_EQ(child_image_kv->second.multiply_color[0], child_color.red());
EXPECT_EQ(child_image_kv->second.multiply_color[1], child_color.green());
EXPECT_EQ(child_image_kv->second.multiply_color[2], child_color.blue());
EXPECT_EQ(child_image_kv->second.multiply_color[3], child_color.alpha());
// Grab the data for the matrices.
auto matrix_kv = uber_struct->local_matrices.find(rect_handle);
EXPECT_NE(matrix_kv, uber_struct->local_matrices.end());
auto child_matrix_kv = uber_struct->local_matrices.find(child_rect_handle);
EXPECT_NE(child_matrix_kv, uber_struct->local_matrices.end());
// Make sure the values match.
EXPECT_EQ(static_cast<uint32_t>(matrix_kv->second[0][0]), kFilledWidth);
EXPECT_EQ(static_cast<uint32_t>(matrix_kv->second[1][1]), kFilledHeight);
EXPECT_EQ(static_cast<uint32_t>(child_matrix_kv->second[0][0]), kFilledChildWidth);
EXPECT_EQ(static_cast<uint32_t>(child_matrix_kv->second[1][1]), kFilledChildHeight);
}
TEST_F(FlatlandTest, SetImageSampleRegionTestCases) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
const TransformId kTransformId = {1};
const ContentId kId = {3};
const uint32_t kImageWidth = 854;
const uint32_t kImageHeight = 480;
// Zero is not a valid content ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageSampleRegion({0}, types::RectangleF({0, 0, kImageWidth, kImageHeight}));
PRESENT(flatland, false);
}
// The content id hasn't been imported yet.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetImageSampleRegion(kId, types::RectangleF({0, 0, kImageWidth, kImageHeight}));
PRESENT(flatland, false);
}
// Setup a valid transform and image.
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(fuchsia_math::SizeU{kImageWidth, kImageHeight});
CreateImage(flatland.get(), allocator.get(), kId, std::move(ref_pair), std::move(properties));
flatland->SetContent(kTransformId, kId);
PRESENT(flatland, true);
// Testing now with good values should finally work.
{
flatland->SetImageSampleRegion(kId, types::RectangleF({0, 0, kImageWidth, kImageHeight}));
PRESENT(flatland, true);
const float kYDeltaCoefficient = 0.370833f;
const float kHeightCoefficient = 0.629167f;
EXPECT_EQ(1.f, kYDeltaCoefficient + kHeightCoefficient);
const float kYDelta = kYDeltaCoefficient * kImageHeight;
const float kYHeight = kHeightCoefficient * kImageHeight;
// Note that comparing these values using == operator as floats fails due to the loss of
// precision.
EXPECT_GT(kYDelta + kYHeight, static_cast<float>(kImageHeight));
EXPECT_TRUE(escher::CompareFloat(kYDelta + kYHeight, static_cast<float>(kImageHeight), 0.01f));
flatland->SetImageSampleRegion(kId, types::RectangleF({0, kYDelta, kImageWidth, kYHeight}));
PRESENT(flatland, true);
}
// Test one more time with out of bounds values and it should fail.
{
// (x + width) exceeeds the image width and should fail.
flatland->SetImageSampleRegion(kId, types::RectangleF({1, 0, kImageWidth, kImageHeight}));
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetClipBoundaryErrorCases) {
const TransformId kTransformId = {1};
// Zero is not a valid transform ID.
{
fuchsia_math::Rect rect{0, 0, 20, 30};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetClipBoundary({0}, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, false);
}
// Transform ID is valid but not yet imported
{
fuchsia_math::Rect rect{0, 0, 20, 30};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetClipBoundary(kTransformId, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, false);
}
// Width must be positive.
{
fuchsia_math::Rect rect{0, 0, 20, 30};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetClipBoundary(kTransformId, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, true);
const auto maybe_transform_handle = flatland->GetTransformHandle(kTransformId);
ASSERT_TRUE(maybe_transform_handle.has_value());
const auto transform_handle = maybe_transform_handle.value();
auto uber_struct = GetUberStruct(flatland.get());
auto clip_region_itr = uber_struct->local_clip_regions.find(transform_handle);
EXPECT_NE(clip_region_itr, uber_struct->local_clip_regions.end());
auto clip_region = clip_region_itr->second;
EXPECT_EQ(TransformClipRegion::From(rect), clip_region);
fuchsia_math::Rect rect_bad = {0, 0, -20, 30};
flatland->SetClipBoundary(kTransformId,
std::make_unique<fuchsia_math::Rect>(std::move(rect_bad)));
PRESENT(flatland, false);
}
// Height must be positive.
{
fuchsia_math::Rect rect{0, 0, 20, 30};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kTransformId);
flatland->SetClipBoundary(kTransformId, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, true);
fuchsia_math::Rect rect_bad = {0, 0, 20, -30};
flatland->SetClipBoundary(kTransformId,
std::make_unique<fuchsia_math::Rect>(std::move(rect_bad)));
PRESENT(flatland, false);
}
// Can't overflow on the X-axis.
{
fuchsia_math::Rect rect = {INT_MAX - 1, 0, INT_MAX - 1, 30};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kTransformId);
flatland->SetClipBoundary(kTransformId, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, false);
}
// Can't overflow on the Y-axis.
{
fuchsia_math::Rect rect = {0, INT_MAX - 1, 30, INT_MAX - 1};
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kTransformId);
flatland->SetClipBoundary(kTransformId, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, false);
}
// Null value is OK.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
// Grab the transform handle.
const auto maybe_transform_handle = flatland->GetTransformHandle(kTransformId);
ASSERT_TRUE(maybe_transform_handle.has_value());
const auto transform_handle = maybe_transform_handle.value();
// Set a null value.
flatland->SetClipBoundary(kTransformId, nullptr);
PRESENT(flatland, true);
// Check that there is no clip region in the uber struct.
auto uber_struct = GetUberStruct(flatland.get());
auto clip_region_itr = uber_struct->local_clip_regions.find(transform_handle);
EXPECT_EQ(clip_region_itr, uber_struct->local_clip_regions.end());
// Set a proper value.
fuchsia_math::Rect rect = {10, 30, 20, 90};
flatland->SetClipBoundary(kTransformId, std::make_unique<fuchsia_math::Rect>(std::move(rect)));
PRESENT(flatland, true);
// Check that this value has now made its way to the uber struct.
uber_struct = GetUberStruct(flatland.get());
clip_region_itr = uber_struct->local_clip_regions.find(transform_handle);
EXPECT_NE(clip_region_itr, uber_struct->local_clip_regions.end());
auto clip_region = clip_region_itr->second;
EXPECT_EQ(TransformClipRegion::From(rect), clip_region);
// Set it to be null again.
flatland->SetClipBoundary(kTransformId, nullptr);
PRESENT(flatland, true);
// Now check that its not in the uber struct anymore.
uber_struct = GetUberStruct(flatland.get());
clip_region_itr = uber_struct->local_clip_regions.find(transform_handle);
EXPECT_EQ(clip_region_itr, uber_struct->local_clip_regions.end());
}
}
TEST_F(FlatlandTest, CreateImageErrorCases) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
// Default image properties.
const uint32_t kDefaultVmoIndex = 1;
const uint32_t kDefaultWidth = 100;
const uint32_t kDefaultHeight = 1000;
// Setup a valid buffer collection.
auto ref_pair = BufferCollectionImportExportTokens::New();
REGISTER_BUFFER_COLLECTION(allocator, ref_pair.export_token, CreateToken(), true);
// Zero is not a valid image ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateImage({0}, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
ImageProperties());
PRESENT(flatland, false);
}
// The import token must also be valid.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateImage({1}, BufferCollectionImportToken(), kDefaultVmoIndex, ImageProperties());
PRESENT(flatland, false);
}
// The buffer collection can fail to create an image.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateImage({1}, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
ImageProperties());
PRESENT(flatland, false);
}
// Size must be set.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->CreateImage({1}, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
ImageProperties());
PRESENT(flatland, false);
}
// Width cannot be 0.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
ImageProperties properties;
properties.size(SizeU{0, 1});
flatland->CreateImage({1}, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
std::move(properties));
PRESENT(flatland, false);
}
// Height cannot be 0.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
ImageProperties properties;
properties.size(SizeU{1, 0});
flatland->CreateImage({1}, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
std::move(properties));
PRESENT(flatland, false);
}
// Check to make sure that if the BufferCollectionImporter returns false, then the call
// to Flatland::CreateImage() also returns false.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
const ContentId kId = {100};
ImageProperties properties;
properties.size(SizeU{kDefaultWidth, kDefaultHeight});
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(false));
flatland->CreateImage(kId, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
std::move(properties));
PRESENT(flatland, false);
}
// Two images cannot have the same ID.
const ContentId kId = {1};
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
{
ImageProperties properties;
properties.size(SizeU{kDefaultWidth, kDefaultHeight});
// This is the first call in these series of test components that makes it down to
// the BufferCollectionImporter. We have to make sure it returns true here so that
// the test doesn't erroneously fail.
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _))
.WillOnce(Return(true));
flatland->CreateImage(kId, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
std::move(properties));
PRESENT(flatland, true);
}
{
ImageProperties properties;
properties.size(SizeU{kDefaultWidth, kDefaultHeight});
// We shouldn't even make it to the BufferCollectionImporter here due to the duplicate
// ID causing CreateImage() to return early.
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).Times(0);
flatland->CreateImage(kId, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
std::move(properties));
PRESENT(flatland, false);
}
}
// A Link id cannot be used for an image.
const ContentId kLinkId = {2};
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties link_properties;
link_properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport(kLinkId, std::move(parent_token), std::move(link_properties),
std::move(child_view_watcher_server_end));
PRESENT(flatland, true);
ImageProperties image_properties;
image_properties.size(SizeU{kDefaultWidth, kDefaultHeight});
flatland->CreateImage(kLinkId, ref_pair.DuplicateImportToken(), kDefaultVmoIndex,
std::move(image_properties));
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, CreateImageWithDuplicatedImportTokens) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
auto ref_pair = BufferCollectionImportExportTokens::New();
REGISTER_BUFFER_COLLECTION(allocator, ref_pair.export_token, CreateToken(), true);
const uint64_t kNumImages = 3;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _))
.Times(kNumImages)
.WillRepeatedly(Return(true));
for (uint64_t i = 0; i < kNumImages; ++i) {
ImageProperties properties;
properties.size(SizeU{150, 175});
flatland->CreateImage(/*image_id*/ {i + 1}, ref_pair.DuplicateImportToken(), /*vmo_idx*/ i,
std::move(properties));
PRESENT(flatland, true);
}
}
TEST_F(FlatlandTest, CreateImageInMultipleFlatlands) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland1 = CreateFlatland();
std::shared_ptr<Flatland> flatland2 = CreateFlatland();
auto ref_pair = BufferCollectionImportExportTokens::New();
REGISTER_BUFFER_COLLECTION(allocator, ref_pair.export_token, CreateToken(), true);
// We can import the same image in both flatland instances.
{
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(true));
ImageProperties properties;
properties.size(SizeU{150, 175});
flatland1->CreateImage({1}, ref_pair.DuplicateImportToken(), 0, std::move(properties));
PRESENT(flatland1, true);
}
{
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(true));
ImageProperties properties;
properties.size(SizeU{150, 175});
flatland2->CreateImage({1}, ref_pair.DuplicateImportToken(), 0, std::move(properties));
PRESENT(flatland2, true);
}
// There are seperate ReleaseBufferImage calls to release them from importers.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(2);
flatland1->Clear();
PRESENT(flatland1, true);
flatland2->Clear();
PRESENT(flatland2, true);
}
TEST_F(FlatlandTest, SetContentErrorCases) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
const uint32_t kWidth = 100;
const uint32_t kHeight = 200;
ImageProperties properties;
properties.size(SizeU{kWidth, kHeight});
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties));
// Create a transform.
const TransformId kTransformId = {1};
flatland->CreateTransform(kTransformId);
PRESENT(flatland, true);
// Zero is not a valid transform.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetContent({0}, kImageId);
PRESENT(flatland, false);
}
// The transform must exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetContent({2}, kImageId);
PRESENT(flatland, false);
}
// The image must exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->SetContent(kTransformId, {2});
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, ClearContentOnTransform) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{100, 200});
auto import_token_dup = ref_pair.DuplicateImportToken();
auto global_collection_id = CreateImage(flatland.get(), allocator.get(), kImageId,
std::move(ref_pair), std::move(properties))
.collection_id;
const auto maybe_image_handle = flatland->GetContentHandle(kImageId);
ASSERT_TRUE(maybe_image_handle.has_value());
const auto image_handle = maybe_image_handle.value();
// Create a transform, make it the root transform, and attach the image.
const TransformId kTransformId = {1};
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
PRESENT(flatland, true);
// The image handle should be the last handle in the local_topology, and the image should be in
// the image map.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, image_handle);
auto image_kv = uber_struct->images.find(image_handle);
EXPECT_NE(image_kv, uber_struct->images.end());
EXPECT_EQ(image_kv->second.collection_id, global_collection_id);
// An ContentId of 0 indicates to remove any content on the specified transform.
flatland->SetContent(kTransformId, {0});
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
for (const auto& entry : uber_struct->local_topology) {
EXPECT_NE(entry.handle, image_handle);
}
}
TEST_F(FlatlandTest, SetTheSameContentOnMultipleTransforms) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{100, 200});
auto import_token_dup = ref_pair.DuplicateImportToken();
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties));
// Create a transform, make it the root transform, and add two children.
const TransformId kTransformId1 = {1};
const TransformId kTransformId2 = {2};
const TransformId kTransformId3 = {3};
flatland->CreateTransform(kTransformId1);
flatland->CreateTransform(kTransformId2);
flatland->CreateTransform(kTransformId3);
flatland->SetRootTransform(kTransformId1);
flatland->AddChild(kTransformId1, kTransformId2);
flatland->AddChild(kTransformId1, kTransformId3);
// Set the same content on both children
flatland->SetContent(kTransformId2, kImageId);
flatland->SetContent(kTransformId3, kImageId);
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, TopologyVisitsContentBeforeChildren) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup two valid images.
const ContentId kImageId1 = {1};
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
CreateImage(flatland.get(), allocator.get(), kImageId1, std::move(ref_pair_1),
std::move(properties1));
const auto maybe_image_handle1 = flatland->GetContentHandle(kImageId1);
ASSERT_TRUE(maybe_image_handle1.has_value());
const auto image_handle1 = maybe_image_handle1.value();
const ContentId kImageId2 = {2};
auto ref_pair_2 = BufferCollectionImportExportTokens::New();
ImageProperties properties2;
properties2.size(SizeU{300, 400});
CreateImage(flatland.get(), allocator.get(), kImageId2, std::move(ref_pair_2),
std::move(properties2));
const auto maybe_image_handle2 = flatland->GetContentHandle(kImageId2);
ASSERT_TRUE(maybe_image_handle2.has_value());
const auto image_handle2 = maybe_image_handle2.value();
// Create a root transform with two children.
const TransformId kTransformId1 = {3};
const TransformId kTransformId2 = {4};
const TransformId kTransformId3 = {5};
flatland->CreateTransform(kTransformId1);
flatland->CreateTransform(kTransformId2);
flatland->CreateTransform(kTransformId3);
flatland->AddChild(kTransformId1, kTransformId2);
flatland->AddChild(kTransformId1, kTransformId3);
flatland->SetRootTransform(kTransformId1);
PRESENT(flatland, true);
// Attach image 1 to the root and the second child-> Attach image 2 to the first child->
flatland->SetContent(kTransformId1, kImageId1);
flatland->SetContent(kTransformId2, kImageId2);
flatland->SetContent(kTransformId3, kImageId1);
PRESENT(flatland, true);
// The images should appear pre-order toplogically sorted: 1, 2, 1 again. The same image is
// allowed to appear multiple times.
std::queue<TransformHandle> expected_handle_order;
expected_handle_order.push(image_handle1);
expected_handle_order.push(image_handle2);
expected_handle_order.push(image_handle1);
auto uber_struct = GetUberStruct(flatland.get());
for (const auto& entry : uber_struct->local_topology) {
if (entry.handle == expected_handle_order.front()) {
expected_handle_order.pop();
}
}
EXPECT_TRUE(expected_handle_order.empty());
// Clearing the image from the parent removes the first entry of the list since images are
// visited before children.
flatland->SetContent(kTransformId1, {0});
PRESENT(flatland, true);
// Meaning the new list of images should be: 2, 1.
expected_handle_order.push(image_handle2);
expected_handle_order.push(image_handle1);
uber_struct = GetUberStruct(flatland.get());
for (const auto& entry : uber_struct->local_topology) {
if (entry.handle == expected_handle_order.front()) {
expected_handle_order.pop();
}
}
EXPECT_TRUE(expected_handle_order.empty());
}
// Tests that a buffer collection is released after CreateImage() if there are no more import
// tokens.
TEST_F(FlatlandTest, ReleaseBufferCollectionHappensAfterCreateImage) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Register a valid buffer collection.
auto ref_pair = BufferCollectionImportExportTokens::New();
REGISTER_BUFFER_COLLECTION(allocator, ref_pair.export_token, CreateToken(), true);
const ContentId kImageId = {1};
ImageProperties properties;
properties.size(SizeU{100, 200});
// Send our only import token to CreateImage(). Buffer collection should be released only after
// Image creation.
{
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(true));
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(_, _)).Times(1);
flatland->CreateImage(kImageId, std::move(ref_pair.import_token), 0, std::move(properties));
RunLoopUntilIdle();
}
}
// In this variation, the image is explicitly released, and the internally-generated release fence
// is signaled after because we don't set `args.skip_session_update_and_release_fences` as we do
// in the other variations.
TEST_F(FlatlandTest, ReleaseBufferCollectionCompletesAfterFlatlandDestruction1) {
allocation::GlobalBufferCollectionId global_collection_id;
display::ImageId global_image_id;
{
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
const ContentId kImageId = {3};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{200, 200});
auto import_token_dup = ref_pair.DuplicateImportToken();
auto global_id_pair = CreateImage(flatland.get(), allocator.get(), kImageId,
std::move(ref_pair), std::move(properties));
global_collection_id = global_id_pair.collection_id;
global_image_id = global_id_pair.image_id;
// Release the image.
flatland->ReleaseImage(kImageId);
// Release the buffer collection.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id, _))
.Times(1);
import_token_dup.value().reset();
RunLoopUntilIdle();
// Present, apply updates, and signal fences so that we can verify that the image is released.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(global_image_id)).Times(1);
{
PRESENT_WITH_ARGS(flatland, PresentArgs{}, true);
}
RunLoopUntilIdle();
// |flatland| is about to fall out of scope. Ensure that no image is released, because it
// always was earlier..
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(global_image_id)).Times(0);
}
}
// In this variation, the image is explicitly released, but the BufferCollectionImporter isn't
// notified until the Flatland session is destroyed. This exercises the logic in ~Flatland(), and
// verifies that we don't call ReleaseBufferImage() twice for the same image.
TEST_F(FlatlandTest, ReleaseBufferCollectionCompletesAfterFlatlandDestruction2) {
allocation::GlobalBufferCollectionId global_collection_id;
display::ImageId global_image_id;
{
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
const ContentId kImageId = {3};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{200, 200});
auto import_token_dup = ref_pair.DuplicateImportToken();
auto global_id_pair = CreateImage(flatland.get(), allocator.get(), kImageId,
std::move(ref_pair), std::move(properties));
global_collection_id = global_id_pair.collection_id;
global_image_id = global_id_pair.image_id;
// Release the image.
flatland->ReleaseImage(kImageId);
// Release the buffer collection.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id, _))
.Times(1);
import_token_dup.value().reset();
RunLoopUntilIdle();
// Skip session updates to test that release fences are what trigger the importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(global_image_id)).Times(0);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
{
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// |flatland| is about to fall out of scope. Ensure that the images is released.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(global_image_id)).Times(1);
}
}
// This variation is similar to ReleaseBufferCollectionCompletesAfterFlatlandDestruction1, except
// that the image has not been released by the client by the time that the Flatland session is
// destroyed.
TEST_F(FlatlandTest, ReleaseBufferCollectionCompletesAfterFlatlandDestruction3) {
allocation::GlobalBufferCollectionId global_collection_id;
display::ImageId global_image_id;
{
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
const ContentId kImageId = {3};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{200, 200});
auto import_token_dup = ref_pair.DuplicateImportToken();
auto global_id_pair = CreateImage(flatland.get(), allocator.get(), kImageId,
std::move(ref_pair), std::move(properties));
global_collection_id = global_id_pair.collection_id;
global_image_id = global_id_pair.image_id;
// Release the buffer collection.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id, _))
.Times(1);
import_token_dup.value().reset();
RunLoopUntilIdle();
// Skip session updates to test that release fences are what trigger the importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(global_image_id)).Times(0);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
{
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// |flatland| is about to fall out of scope. Ensure that the image is released.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(global_image_id)).Times(1);
}
}
// Tests that an Image is not released from the importer until it is not referenced and the
// release fence is signaled.
TEST_F(FlatlandTest, ReleaseImageWaitsForReleaseFence) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid buffer collection and Image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties;
properties.size(SizeU{100, 200});
auto import_token_dup = ref_pair.DuplicateImportToken();
const auto global_id_pair = CreateImage(flatland.get(), allocator.get(), kImageId,
std::move(ref_pair), std::move(properties));
auto& global_collection_id = global_id_pair.collection_id;
// Attach the Image to a transform.
const TransformId kTransformId = {3};
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
PRESENT(flatland, true);
// Release the buffer collection, but ensure that the ReleaseBufferImage call on the importer
// has not happened.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id, _))
.Times(1);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(0);
import_token_dup.value().reset();
RunLoopUntilIdle();
// Release the Image that referenced the buffer collection. Because the Image is still attached
// to a Transform, the deregestration call should still not happen.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(0);
flatland->ReleaseImage(kImageId);
PRESENT(flatland, true);
// Remove the Image from the transform. This triggers the creation of the release fence, but
// still does not result in a deregestration call. Skip session updates to test that release
// fences are what trigger the importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(0);
flatland->SetContent(kTransformId, {0});
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
// Signal the release fences, which triggers the release call.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(1);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, ReleaseImageErrorCases) {
// Zero is not a valid image ID.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseImage({0});
PRESENT(flatland, false);
}
// The image must exist.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
flatland->ReleaseImage({1});
PRESENT(flatland, false);
}
// ContentId is not an Image.
{
std::shared_ptr<Flatland> flatland = CreateFlatland();
ViewportCreationToken parent_token;
ViewCreationToken child_token;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value(), &child_token.value()));
const ContentId kLinkId = {2};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
flatland->CreateViewport(kLinkId, std::move(parent_token), std::move(properties),
std::move(child_view_watcher_server_end));
flatland->ReleaseImage(kLinkId);
PRESENT(flatland, false);
}
}
// If we have multiple BufferCollectionImporters, some of them may properly import
// an image while others do not. We have to therefore make sure that if importer A
// properly imports an image and then importer B fails, that Flatland automatically
// releases the image from importer A.
TEST_F(FlatlandTest, ImageImportPassesAndFailsOnDifferentImportersTest) {
// Create a second buffer collection importer.
auto local_mock_buffer_collection_importer = new MockBufferCollectionImporter();
auto local_buffer_collection_importer =
std::shared_ptr<allocation::BufferCollectionImporter>(local_mock_buffer_collection_importer);
// Create flatland and allocator instances that has two BufferCollectionImporters.
std::vector<std::shared_ptr<allocation::BufferCollectionImporter>> importers(
{buffer_collection_importer_, local_buffer_collection_importer});
std::vector<std::shared_ptr<allocation::BufferCollectionImporter>> screenshot_importers;
std::shared_ptr<Allocator> allocator = std::make_shared<Allocator>(
context_provider_.context(), importers, screenshot_importers,
utils::CreateSysmemAllocatorSyncPtr("ImageImportPassesFailsOnDiffImportersTest"));
auto session_id = scheduling::GetNextSessionId();
auto [flatland_client_end, flatland_server_end] =
fidl::Endpoints<fuchsia_ui_composition::Flatland>::Create();
auto flatland = Flatland::New(
std::make_shared<utils::UnownedDispatcherHolder>(dispatcher()),
std::move(flatland_server_end), session_id,
/*destroy_instance_functon=*/[]() {}, flatland_presenter_, link_system_,
uber_struct_system_->AllocateQueueForSession(session_id), importers, [](auto...) {},
[](auto...) {}, [](auto...) {}, [](auto...) {},
fuchsia_ui_composition::TrustedFlatlandConfig());
// Wait for Bind() to occur within Flatland::New().
RunLoopUntilIdle();
EXPECT_CALL(*local_mock_buffer_collection_importer, ImportBufferCollection(_, _, _, _, _))
.WillOnce(Return(true));
auto ref_pair = BufferCollectionImportExportTokens::New();
REGISTER_BUFFER_COLLECTION(allocator, ref_pair.export_token, CreateToken(), true);
ImageProperties properties;
properties.size(SizeU{100, 200});
// We have the first importer return true, signifying a successful import, and the second one
// returning false. This should trigger the first importer to call ReleaseBufferImage().
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(true));
EXPECT_CALL(*local_mock_buffer_collection_importer, ImportBufferImage(_, _))
.WillOnce(Return(false));
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).WillOnce(Return());
flatland->CreateImage(/*image_id*/ {1}, std::move(ref_pair.import_token), /*vmo_idx*/ 0,
std::move(properties));
}
// Test to make sure that if a buffer collection importer returns |false|
// on |ImportBufferImage()| that this is caught when we try to present.
TEST_F(FlatlandTest, BufferImporterImportImageReturnsFalseTest) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
auto ref_pair = BufferCollectionImportExportTokens::New();
REGISTER_BUFFER_COLLECTION(allocator, ref_pair.export_token, CreateToken(), true);
// Create a proper properties struct.
ImageProperties properties1;
properties1.size(SizeU{150, 175});
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(true));
// We've imported a proper image and we have the importer returning true, so
// PRESENT should return true.
flatland->CreateImage(/*image_id*/ {1}, ref_pair.DuplicateImportToken(), /*vmo_idx*/ 0,
std::move(properties1));
PRESENT(flatland, true);
// We're using the same buffer collection so we don't need to validate, only import.
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).WillOnce(Return(false));
// Import again, but this time have the importer return false. Flatland should catch
// this and PRESENT should return false.
ImageProperties properties2;
properties2.size(SizeU{150, 175});
flatland->CreateImage(/*image_id*/ {2}, ref_pair.DuplicateImportToken(), /*vmo_idx*/ 0,
std::move(properties2));
PRESENT(flatland, false);
}
// Test to make sure that the release fences signal to the buffer importer
// to release the image.
TEST_F(FlatlandTest, BufferImporterImageReleaseTest) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties1))
.collection_id;
// Create a transform, make it the root transform, and attach the image.
const TransformId kTransformId = {2};
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
PRESENT(flatland, true);
// Now release the image.
flatland->ReleaseImage(kImageId);
PRESENT(flatland, true);
// Now remove the image from the transform, which should result in it being
// garbage collected.
flatland->SetContent(kTransformId, {0});
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(1);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, ReleasedImageRemainsUntilCleared) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
const allocation::GlobalBufferCollectionId global_collection_id =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties1))
.collection_id;
const auto maybe_image_handle = flatland->GetContentHandle(kImageId);
ASSERT_TRUE(maybe_image_handle.has_value());
const auto image_handle = maybe_image_handle.value();
// Create a transform, make it the root transform, and attach the image.
const TransformId kTransformId = {2};
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
PRESENT(flatland, true);
// The image handle should be the last handle in the local_topology, and the image should be in
// the image map.
auto uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, image_handle);
auto image_kv = uber_struct->images.find(image_handle);
EXPECT_NE(image_kv, uber_struct->images.end());
EXPECT_EQ(image_kv->second.collection_id, global_collection_id);
// Releasing the image succeeds, but all data remains in the UberStruct.
flatland->ReleaseImage(kImageId);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, image_handle);
image_kv = uber_struct->images.find(image_handle);
EXPECT_NE(image_kv, uber_struct->images.end());
EXPECT_EQ(image_kv->second.collection_id, global_collection_id);
// Clearing the Transform of its Image removes all references from the UberStruct.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(_)).Times(1);
flatland->SetContent(kTransformId, {0});
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
for (const auto& entry : uber_struct->local_topology) {
EXPECT_NE(entry.handle, image_handle);
}
EXPECT_FALSE(uber_struct->images.count(image_handle));
}
TEST_F(FlatlandTest, ReleasedImageIdCanBeReused) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
const auto maybe_image_handle1 = flatland->GetContentHandle(kImageId);
ASSERT_TRUE(maybe_image_handle1.has_value());
const auto image_handle1 = maybe_image_handle1.value();
// Create a transform, make it the root transform, attach the image, then release it.
const TransformId kTransformId1 = {2};
flatland->CreateTransform(kTransformId1);
flatland->SetRootTransform(kTransformId1);
flatland->SetContent(kTransformId1, kImageId);
flatland->ReleaseImage(kImageId);
PRESENT(flatland, true);
// The ContentId can be re-used even though the old image is still present. Add a second
// transform so that both images show up in the global image vector.
auto ref_pair_2 = BufferCollectionImportExportTokens::New();
ImageProperties properties2;
properties2.size(SizeU{300, 400});
const allocation::GlobalBufferCollectionId global_collection_id2 =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair_2),
std::move(properties2))
.collection_id;
const TransformId kTransformId2 = {3};
flatland->CreateTransform(kTransformId2);
flatland->AddChild(kTransformId1, kTransformId2);
flatland->SetContent(kTransformId2, kImageId);
PRESENT(flatland, true);
const auto maybe_image_handle2 = flatland->GetContentHandle(kImageId);
ASSERT_TRUE(maybe_image_handle2.has_value());
const auto image_handle2 = maybe_image_handle2.value();
// Both images should appear in the image map.
auto uber_struct = GetUberStruct(flatland.get());
auto image_kv1 = uber_struct->images.find(image_handle1);
EXPECT_NE(image_kv1, uber_struct->images.end());
EXPECT_EQ(image_kv1->second.collection_id, global_collection_id1);
auto image_kv2 = uber_struct->images.find(image_handle2);
EXPECT_NE(image_kv2, uber_struct->images.end());
EXPECT_EQ(image_kv2->second.collection_id, global_collection_id2);
}
// Test that released Images, when attached to a Transform, are not garbage collected even if
// the Transform is not part of the most recently presented global topology.
TEST_F(FlatlandTest, ReleasedImagePersistsOutsideGlobalTopology) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair),
std::move(properties1))
.collection_id;
const auto maybe_image_handle = flatland->GetContentHandle(kImageId);
ASSERT_TRUE(maybe_image_handle.has_value());
const auto image_handle = maybe_image_handle.value();
// Create a transform, make it the root transform, attach the image, then release it.
const TransformId kTransformId = {2};
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
flatland->ReleaseImage(kImageId);
PRESENT(flatland, true);
// Remove the entire hierarchy, then verify that the image is still present.
flatland->SetRootTransform({0});
PRESENT(flatland, true);
auto uber_struct = GetUberStruct(flatland.get());
auto image_kv = uber_struct->images.find(image_handle);
EXPECT_NE(image_kv, uber_struct->images.end());
EXPECT_EQ(image_kv->second.collection_id, global_collection_id1);
// Reintroduce the hierarchy and confirm the Image is still present, even though it was
// temporarily not reachable from the root transform.
flatland->SetRootTransform(kTransformId);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland.get());
EXPECT_EQ(uber_struct->local_topology.back().handle, image_handle);
image_kv = uber_struct->images.find(image_handle);
EXPECT_NE(image_kv, uber_struct->images.end());
EXPECT_EQ(image_kv->second.collection_id, global_collection_id1);
}
TEST_F(FlatlandTest, ClearReleasesImagesAndBufferCollections) {
std::shared_ptr<Allocator> allocator = CreateAllocator();
std::shared_ptr<Flatland> flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = {1};
auto ref_pair_1 = BufferCollectionImportExportTokens::New();
ImageProperties properties1;
properties1.size(SizeU{100, 200});
auto import_token_dup = ref_pair_1.DuplicateImportToken();
const allocation::GlobalBufferCollectionId global_collection_id1 =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair_1),
std::move(properties1))
.collection_id;
// Create a transform, make it the root transform, and attach the Image.
const TransformId kTransformId = {2};
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
PRESENT(flatland, true);
// Clear the graph, then signal the release fence and ensure the buffer collection is released.
flatland->Clear();
import_token_dup.value().reset();
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id1, _))
.Times(1);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(display::ImageId(1))).Times(1);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(display::ImageId(2))).Times(1);
PRESENT(flatland, true);
// The Image ID should be available for re-use.
auto ref_pair_2 = BufferCollectionImportExportTokens::New();
ImageProperties properties2;
properties2.size(SizeU{400, 800});
const allocation::GlobalBufferCollectionId global_collection_id2 =
CreateImage(flatland.get(), allocator.get(), kImageId, std::move(ref_pair_2),
std::move(properties2))
.collection_id;
EXPECT_NE(global_collection_id1, global_collection_id2);
// Verify that the Image is valid and can be attached to a transform.
flatland->CreateTransform(kTransformId);
flatland->SetRootTransform(kTransformId);
flatland->SetContent(kTransformId, kImageId);
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, UnsquashableUpdates_ShouldBeReflectedInScheduleUpdates) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
// We call Present() twice, each time passing a different value as the unsquashable argument.
// We EXPECT that the ensuing ScheduleUpdateForSession() call to the frame scheduler will
// reflect the passed in squashable value.
// Present with the unsquashable field set to true.
{
PresentArgs args;
args.unsquashable = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// Present with the unsquashable field set to false.
{
PresentArgs args;
args.unsquashable = false;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
}
TEST_F(FlatlandTest, MultithreadedLinkResolution) {
auto parent_flatland = CreateFlatland();
std::shared_ptr<Flatland> child_flatland;
auto child_flatland_thread_loop_holder =
std::make_shared<utils::LoopDispatcherHolder>(&kAsyncLoopConfigNoAttachToCurrentThread);
auto& child_flatland_thread_loop = child_flatland_thread_loop_holder->loop();
auto [child_flatland_client_end, child_flatland_server_end] =
fidl::Endpoints<fuchsia_ui_composition::Flatland>::Create();
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end_workaround] =
fidl::Endpoints<ParentViewportWatcher>::Create();
// C++-20 workaround, so that this can be captured in the closure below.
fidl::ServerEnd<ParentViewportWatcher> parent_viewport_watcher_server_end(
std::move(parent_viewport_watcher_server_end_workaround));
{
auto session_id = scheduling::GetNextSessionId();
std::vector<std::shared_ptr<BufferCollectionImporter>> importers;
importers.push_back(buffer_collection_importer_);
child_flatland = Flatland::New(
child_flatland_thread_loop_holder, std::move(child_flatland_server_end), session_id,
[](auto...) {}, flatland_presenter_, link_system_,
uber_struct_system_->AllocateQueueForSession(session_id), importers, [](auto...) {},
[](auto...) {}, [](auto...) {}, [](auto...) {},
fuchsia_ui_composition::TrustedFlatlandConfig());
auto status = child_flatland_thread_loop.StartThread();
EXPECT_EQ(status, ZX_OK);
}
auto creation_tokens = scenic::ViewCreationTokenPair::New();
const TransformId kRootTransform = {1};
const ContentId kLinkId = {1};
// One of the link ends needs to run first. Here, we arbitrarily choose it to be the viewport
// end, and wait for it to finish. Of course, the link is not yet resolved, because the view
// hasn't yet been created.
ViewportProperties properties;
properties.logical_size(fuchsia_math::SizeU{kDefaultSize, kDefaultSize});
parent_flatland->CreateTransform(kRootTransform);
parent_flatland->SetRootTransform(kRootTransform);
parent_flatland->CreateViewport(kLinkId, fidl::HLCPPToNatural(creation_tokens.viewport_token),
std::move(properties), std::move(child_view_watcher_server_end));
parent_flatland->SetContent(kRootTransform, kLinkId);
PRESENT(parent_flatland, true);
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
UpdateLinks(parent_flatland->GetRoot());
auto links = link_system_->GetResolvedTopologyLinks();
EXPECT_EQ(links.size(), 0U);
// We post this task onto the other Flatland's thread, so that we can have a *chance* of locking
// the LinkSystem mutex in between the execution of the two link-resolution closures (each of
// which also locks the LinkSystem mutex).
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _));
libsync::Completion completion;
async::PostTask(child_flatland_thread_loop.dispatcher(), ([&]() {
child_flatland->CreateView2(
fidl::HLCPPToNatural(creation_tokens.view_token),
fidl::HLCPPToNatural(scenic::NewViewIdentityOnCreation()),
NoViewProtocols(), std::move(parent_viewport_watcher_server_end));
fuchsia_ui_composition::PresentArgs present_args;
present_args.requested_presentation_time(0)
.acquire_fences({})
.release_fences({})
.unsquashable({});
// `Present()` puts the resulting UberStruct into the "acquire fence queue";
// since there are no fences it is not-quite-immediately made available via a
// posted task.
child_flatland->Present(std::move(present_args));
completion.Signal();
}));
// This runs "concurrently" with the task above. Ideally, it will sometimes fall between the
// running the two ObjectLinker link-resolved closures. Unfortunately this is unlikely, so we
// can't reliably ensure that this case is handled properly. More often, this will occur either
// before both link-resolved closures, or after both of them (both of these situations are
// handled properly).
ApplySessionUpdatesAndSignalFences();
UpdateLinks(parent_flatland->GetRoot());
links = link_system_->GetResolvedTopologyLinks();
// One of these will be true, but we don't know which one.
// EXPECT_EQ(links.size(), 0U);
// EXPECT_EQ(links.size(), 1U);
// By waiting for the task to finish running, we know that the Present() has happened, and
// therefore both the parent and child Flatland sessions will have provided UberStructs, so
// that there will now be a resolved link between the viewport and the view.
completion.Wait();
ApplySessionUpdatesAndSignalFences();
UpdateLinks(parent_flatland->GetRoot());
links = link_system_->GetResolvedTopologyLinks();
EXPECT_EQ(links.size(), 1U);
// Need to destroy the child Flatland on the correct thread, so the FIDL binding doesn't CHECK.
async::PostTask(child_flatland_thread_loop.dispatcher(),
[&, flatland_to_destroy = std::move(child_flatland)]() {
child_flatland_thread_loop.Quit();
});
child_flatland_thread_loop.JoinThreads();
}
// Verify that `destroy_instance_function_()` is only invoked once, even if there are multiple
// errors that each would alone cause `destroy_instance_function_()` to be invoked.
TEST_F(FlatlandTest, NoDoubleDestroyRequest) {
// Create flatland and allocator instances that has two BufferCollectionImporters.
std::vector<std::shared_ptr<allocation::BufferCollectionImporter>> no_importers;
std::shared_ptr<Allocator> allocator =
std::make_shared<Allocator>(context_provider_.context(), no_importers, no_importers,
utils::CreateSysmemAllocatorSyncPtr("NoDoubleDestroyRequest"));
auto session_id = scheduling::GetNextSessionId();
size_t destroy_instance_function_invocation_count = 0;
auto [flatland_client_end, flatland_server_end] =
fidl::Endpoints<fuchsia_ui_composition::Flatland>::Create();
auto flatland = Flatland::New(
std::make_shared<utils::UnownedDispatcherHolder>(dispatcher()),
std::move(flatland_server_end), session_id,
/*destroy_instance_functon=*/
[&destroy_instance_function_invocation_count]() {
++destroy_instance_function_invocation_count;
},
flatland_presenter_, link_system_, uber_struct_system_->AllocateQueueForSession(session_id),
no_importers, [](auto...) {}, [](auto...) {}, [](auto...) {}, [](auto...) {},
fuchsia_ui_composition::TrustedFlatlandConfig());
// Wait for server channel to be bound; see `Flatland::Bind()`.
RunLoopUntilIdle();
fuchsia_ui_composition::PresentArgs present_args;
present_args.requested_presentation_time(0).acquire_fences({}).release_fences({}).unsquashable(
{});
flatland->AddChild(TransformId{11}, TransformId{12});
flatland->Present(std::move(present_args));
EXPECT_EQ(destroy_instance_function_invocation_count, 1U);
flatland->AddChild(TransformId{11}, TransformId{12});
flatland->Present(std::move(present_args));
// If it wasn't for the guard variable `destroy_instance_function_was_invoked_` in
// `Flatland::CloseConnection()`, this check would fail deterministically.
EXPECT_EQ(destroy_instance_function_invocation_count, 1U);
}
TEST_F(FlatlandDisplayTest, SimpleSetContent) {
constexpr uint32_t kWidth = 800;
constexpr uint32_t kHeight = 600;
std::shared_ptr<FlatlandDisplay> display = CreateFlatlandDisplay(kWidth, kHeight);
std::shared_ptr<Flatland> child = CreateFlatland();
const ContentId kLinkId1 = {1};
auto [child_view_watcher_client_end, child_view_watcher_server_end] =
fidl::Endpoints<ChildViewWatcher>::Create();
auto [parent_viewport_watcher_client_end, parent_viewport_watcher_server_end] =
fidl::Endpoints<ParentViewportWatcher>::Create();
fidl::Client parent_viewport_watcher(std::move(parent_viewport_watcher_client_end), dispatcher());
SetDisplayContent(display.get(), child.get(), std::move(child_view_watcher_server_end),
std::move(parent_viewport_watcher_server_end));
std::optional<LayoutInfo> layout_info;
parent_viewport_watcher->GetLayout().ThenExactlyOnce(
[&](ParentViewportWatcher_GetLayoutResult& result) {
ASSERT_TRUE(result.is_ok());
layout_info = result->info();
});
std::optional<ParentViewportStatus> parent_viewport_watcher_status;
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_viewport_watcher_status = result->status();
});
RunLoopUntilIdle();
// LayoutInfo is sent as soon as the content/graph link is established, to allow clients to
// generate their first frame with minimal latency.
ASSERT_TRUE(layout_info.has_value());
EXPECT_EQ(layout_info->logical_size()->width(), kWidth);
EXPECT_EQ(layout_info->logical_size()->height(), kHeight);
// The ParentViewportWatcher's status must wait until the first frame is generated (represented
// here by the call to UpdateLinks() below).
EXPECT_FALSE(parent_viewport_watcher_status.has_value());
UpdateLinks(display->root_transform());
// UpdateLinks() causes us to receive the status notification. The link is considered to be
// disconnected because the child has not yet presented its first frame.
EXPECT_TRUE(parent_viewport_watcher_status.has_value());
EXPECT_EQ(parent_viewport_watcher_status.value(), ParentViewportStatus::kDisconnectedFromDisplay);
parent_viewport_watcher_status.reset();
PRESENT(child, true);
// The status won't change to "connected" until UpdateLinks() is called again.
parent_viewport_watcher->GetStatus().ThenExactlyOnce(
[&](ParentViewportWatcher_GetStatusResult& result) {
ASSERT_TRUE(result.is_ok());
parent_viewport_watcher_status = result->status();
});
RunLoopUntilIdle();
EXPECT_FALSE(parent_viewport_watcher_status.has_value());
UpdateLinks(display->root_transform());
EXPECT_TRUE(parent_viewport_watcher_status.has_value());
EXPECT_EQ(parent_viewport_watcher_status.value(), ParentViewportStatus::kConnectedToDisplay);
}
TEST_F(FlatlandTest, CreateAndReleaseFilledRect) {
std::shared_ptr<Flatland> flatland = CreateFlatland();
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferImage(_, _)).Times(0);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferImage(allocation::kInvalidImageId))
.Times(0);
const ContentId kFilledRectId = {1};
flatland->CreateFilledRect(kFilledRectId);
PRESENT(flatland, true);
flatland->ReleaseFilledRect(kFilledRectId);
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, PresentWithScheduleAsapConfig) {
fuchsia_ui_composition::TrustedFlatlandConfig config;
config.schedule_asap() = true;
std::shared_ptr<Flatland> flatland = CreateFlatland(std::move(config));
EXPECT_CALL(*mock_flatland_presenter_,
ScheduleUpdateForSession(_, _, _, _, /*schedule_asap=*/true));
fuchsia_ui_composition::PresentArgs present_args;
present_args.requested_presentation_time(0);
flatland->Present(std::move(present_args));
RunLoopUntilIdle();
ApplySessionUpdatesAndSignalFences();
}
// TODO(https://fxbug.dev/42156567): other FlatlandDisplayTests that should be written:
// - version of SimpleSetContent where the child presents before SetDisplayContent() is called.
// - call SetDisplayContent() multiple times.
#undef EXPECT_MATRIX
#undef PRESENT
#undef PRESENT_WITH_ARGS
#undef REGISTER_BUFFER_COLLECTION
} // namespace test
} // namespace flatland