blob: c717c3fac8bd75531b9ec1de9109752d560d43dd [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/scenic/lib/flatland/flatland.h"
#include <lib/async/time.h>
#include <lib/fdio/directory.h>
#include <lib/syslog/cpp/macros.h>
#include <limits>
#include <gtest/gtest.h>
#include "fuchsia/ui/scenic/internal/cpp/fidl.h"
#include "lib/gtest/test_loop_fixture.h"
#include "src/lib/fsl/handles/object_info.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/mock_buffer_collection_importer.h"
#include "src/ui/scenic/lib/flatland/tests/mock_flatland_presenter.h"
#include "src/ui/scenic/lib/flatland/tests/mock_renderer.h"
#include "src/ui/scenic/lib/scheduling/frame_scheduler.h"
#include "src/ui/scenic/lib/scheduling/id.h"
#include <glm/gtx/matrix_transform_2d.hpp>
using ::testing::_;
using ::testing::Return;
using BufferCollectionId = flatland::Flatland::BufferCollectionId;
using ContentId = flatland::Flatland::ContentId;
using TransformId = flatland::Flatland::TransformId;
using flatland::BufferCollectionImporter;
using flatland::BufferCollectionMetadata;
using flatland::Flatland;
using flatland::FlatlandPresenter;
using flatland::GlobalMatrixVector;
using flatland::GlobalTopologyData;
using flatland::ImageMetadata;
using flatland::LinkSystem;
using flatland::MockBufferCollectionImporter;
using flatland::MockFlatlandPresenter;
using flatland::TransformGraph;
using flatland::TransformHandle;
using flatland::UberStruct;
using flatland::UberStructSystem;
using fuchsia::ui::scenic::internal::ContentLink;
using fuchsia::ui::scenic::internal::ContentLinkStatus;
using fuchsia::ui::scenic::internal::ContentLinkToken;
using fuchsia::ui::scenic::internal::Flatland_Present_Result;
using fuchsia::ui::scenic::internal::GraphLink;
using fuchsia::ui::scenic::internal::GraphLinkStatus;
using fuchsia::ui::scenic::internal::GraphLinkToken;
using fuchsia::ui::scenic::internal::ImageProperties;
using fuchsia::ui::scenic::internal::LayoutInfo;
using fuchsia::ui::scenic::internal::LinkProperties;
using fuchsia::ui::scenic::internal::Orientation;
using fuchsia::ui::scenic::internal::Vec2;
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;
// Arguments to the PRESENT_WITH_ARGS macro.
bool skip_session_update_and_release_fences = false;
};
struct GlobalIdPair {
sysmem_util::GlobalBufferCollectionId collection_id;
flatland::GlobalImageId image_id;
};
} // namespace
// 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(); \
if (expect_success) { \
EXPECT_CALL(*mock_flatland_presenter_, \
RegisterPresent(flatland.GetRoot().GetInstanceId(), _)); \
} \
bool processed_callback = false; \
flatland.Present(args.requested_presentation_time.get(), std::move(args.acquire_fences), \
std::move(args.release_fences), [&](Flatland_Present_Result result) { \
EXPECT_EQ(!expect_success, result.is_err()); \
if (expect_success) { \
EXPECT_EQ(1u, result.response().num_presents_remaining); \
} else { \
EXPECT_EQ(fuchsia::ui::scenic::internal::Error::BAD_OPERATION, \
result.err()); \
} \
processed_callback = true; \
}); \
EXPECT_TRUE(processed_callback); \
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, _)); \
} \
RunLoopUntilIdle(); \
if (!args.skip_session_update_and_release_fences) { \
ApplySessionUpdatesAndSignalFences(); \
} \
} \
}
// 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); }
// 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; \
} \
} \
}
namespace {
// TODO(fxbug.dev/56879): consolidate the following 3 helper functions when splitting escher into
// multiple libraries.
zx::event CreateEvent() {
zx::event event;
FX_CHECK(zx::event::create(0, &event) == ZX_OK);
return event;
}
std::vector<zx::event> CreateEventArray(size_t n) {
std::vector<zx::event> events;
for (size_t i = 0; i < n; i++) {
events.push_back(CreateEvent());
}
return events;
}
zx::event CopyEvent(const zx::event& event) {
zx::event event_copy;
if (event.duplicate(ZX_RIGHT_SAME_RIGHTS, &event_copy) != ZX_OK) {
FX_LOGS(ERROR) << "Copying zx::event failed.";
}
return event_copy;
}
bool IsEventSignaled(const zx::event& fence, zx_signals_t signal) {
zx_signals_t pending = 0u;
fence.wait_one(signal, zx::time(), &pending);
return (pending & signal) != 0u;
}
const float kDefaultSize = 1.f;
const glm::vec2 kDefaultPixelScale = {1.f, 1.f};
float GetOrientationAngle(fuchsia::ui::scenic::internal::Orientation orientation) {
switch (orientation) {
case Orientation::CCW_0_DEGREES:
return 0.f;
case Orientation::CCW_90_DEGREES:
return glm::half_pi<float>();
case Orientation::CCW_180_DEGREES:
return glm::pi<float>();
case Orientation::CCW_270_DEGREES:
return glm::three_over_two_pi<float>();
}
}
class FlatlandTest : public gtest::TestLoopFixture {
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_, RegisterPresent(_, _))
.WillByDefault(::testing::Invoke(
[&](scheduling::SessionId session_id, std::vector<zx::event> release_fences) {
const auto next_present_id = scheduling::GetNextPresentId();
// Store all release fences.
pending_release_fences_[{session_id, next_present_id}] = std::move(release_fences);
return next_present_id;
}));
ON_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _))
.WillByDefault(::testing::Invoke(
[&](zx::time requested_presentation_time, scheduling::SchedulingIdPair id_pair) {
// The ID must be already registered.
EXPECT_TRUE(pending_release_fences_.find(id_pair) != pending_release_fences_.end());
// Ensure IDs are strictly increasing.
auto current_id_kv = pending_session_updates_.find(id_pair.session_id);
EXPECT_TRUE(current_id_kv == pending_session_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_session_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;
}));
flatland_presenter_ = std::shared_ptr<FlatlandPresenter>(mock_flatland_presenter_);
mock_buffer_collection_importer_ = new MockBufferCollectionImporter();
buffer_collection_importer_ =
std::shared_ptr<BufferCollectionImporter>(mock_buffer_collection_importer_);
}
void TearDown() override {
RunLoopUntilIdle();
auto link_topologies = link_system_->GetResolvedTopologyLinks();
EXPECT_TRUE(link_topologies.empty());
buffer_collection_importer_.reset();
flatland_presenter_.reset();
// Move the channel to a local variable which will go out of scope
// and close when this function returns.
auto local = local_.release();
}
Flatland CreateFlatland() {
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator;
auto session_id = scheduling::GetNextSessionId();
return Flatland(session_id, flatland_presenter_, link_system_,
uber_struct_system_->AllocateQueueForSession(session_id),
{buffer_collection_importer_}, std::move(sysmem_allocator));
}
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> CreateToken() {
zx::channel remote;
zx::channel::create(0, &local_, &remote);
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token{std::move(remote)};
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_->UpdateSessions(pending_session_updates_);
// Signal all release fences up to and including the PresentId in |pending_session_updates_|.
for (const auto& [session_id, present_id] : pending_session_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_session_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_session_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;
}
void SetDisplayPixelScale(const glm::vec2& pixel_scale) { display_pixel_scale_ = pixel_scale; }
// 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(const 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::UpdateLinks().
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_->UpdateLinks(data.topology_vector, data.live_handles, matrices,
display_pixel_scale_, snapshot);
// Run the looper again to process any queued FIDL events (i.e., Link callbacks).
RunLoopUntilIdle();
}
void CreateLink(Flatland* parent, Flatland* child, ContentId id,
fidl::InterfacePtr<ContentLink>* content_link,
fidl::InterfacePtr<GraphLink>* graph_link) {
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
parent->CreateLink(id, std::move(parent_token), std::move(properties),
content_link->NewRequest());
child->LinkToParent(std::move(child_token), graph_link->NewRequest());
PRESENT((*parent), true);
PRESENT((*child), true);
}
// 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, ContentId image_id, BufferCollectionId collection_id,
ImageProperties properties) {
sysmem_util::GlobalBufferCollectionId global_collection_id = sysmem_util::kInvalidId;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(testing::Invoke(
[&global_collection_id](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id = collection_id;
return true;
}));
flatland->RegisterBufferCollection(collection_id, CreateToken());
EXPECT_NE(global_collection_id, sysmem_util::kInvalidId);
// Ensure all buffer constraints are valid for the desired image by generating constraints based
// on the image properties.
BufferCollectionMetadata metadata;
metadata.vmo_count = 1;
FX_DCHECK(properties.has_width());
metadata.image_constraints.min_coded_width = properties.width();
metadata.image_constraints.max_coded_width = properties.width();
FX_DCHECK(properties.has_height());
metadata.image_constraints.min_coded_height = properties.height();
metadata.image_constraints.max_coded_height = properties.height();
flatland::GlobalImageId global_image_id;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportImage(_))
.WillOnce(testing::Invoke([&global_image_id](const ImageMetadata& meta_data) {
global_image_id = meta_data.identifier;
return true;
}));
flatland->CreateImage(image_id, collection_id, 0, std::move(properties));
PRESENT((*flatland), true);
return {.collection_id = global_collection_id, image_id = global_image_id};
}
protected:
::testing::StrictMock<MockFlatlandPresenter>* mock_flatland_presenter_;
MockBufferCollectionImporter* mock_buffer_collection_importer_;
std::shared_ptr<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_;
private:
glm::vec2 display_pixel_scale_ = kDefaultPixelScale;
// 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_session_updates_;
zx::channel local_;
};
} // namespace
namespace flatland {
namespace test {
TEST_F(FlatlandTest, PresentShouldReturnOne) {
Flatland flatland = CreateFlatland();
PRESENT(flatland, true);
}
TEST_F(FlatlandTest, PresentWaitsForAcquireFences) {
Flatland flatland = CreateFlatland();
// Create two events to serve as acquire fences.
PresentArgs args;
args.acquire_fences = CreateEventArray(2);
auto acquire1_copy = CopyEvent(args.acquire_fences[0]);
auto acquire2_copy = CopyEvent(args.acquire_fences[1]);
// Create an event to serve as a release fence.
args.release_fences = CreateEventArray(1);
auto release_copy = CopyEvent(args.release_fences[0]);
// Because the Present includes acquire fences, it should only be registered with the
// FlatlandPresenter. 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(), 1ul);
EXPECT_EQ(GetUberStruct(flatland), nullptr);
EXPECT_FALSE(IsEventSignaled(release_copy, ZX_EVENT_SIGNALED));
// Signal the second fence and ensure the Present is still registered, 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(), 1ul);
EXPECT_EQ(GetUberStruct(flatland), nullptr);
EXPECT_FALSE(IsEventSignaled(release_copy, ZX_EVENT_SIGNALED));
// Signal the first fence and ensure the Present is no longer registered (because it has been
// applied), 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();
ApplySessionUpdatesAndSignalFences();
registered_presents = GetRegisteredPresents(flatland.GetRoot().GetInstanceId());
EXPECT_TRUE(registered_presents.empty());
EXPECT_NE(GetUberStruct(flatland), nullptr);
EXPECT_TRUE(IsEventSignaled(release_copy, ZX_EVENT_SIGNALED));
}
TEST_F(FlatlandTest, PresentForwardsRequestedPresentationTime) {
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 = CreateEventArray(1);
auto acquire_copy = CopyEvent(args.acquire_fences[0]);
// Because the Present includes acquire fences, it should only be registered with the
// FlatlandPresenter. There should be no requested presentation time.
PRESENT_WITH_ARGS(flatland, std::move(args), true);
auto registered_presents = GetRegisteredPresents(flatland.GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 1ul);
const auto id_pair = scheduling::SchedulingIdPair({
.session_id = flatland.GetRoot().GetInstanceId(),
.present_id = registered_presents[0],
});
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) {
Flatland flatland = CreateFlatland();
// Create an event to serve as the acquire fence.
PresentArgs args;
args.acquire_fences = CreateEventArray(1);
auto acquire_copy = CopyEvent(args.acquire_fences[0]);
// Create an event to serve as a release fence.
args.release_fences = CreateEventArray(1);
auto release_copy = 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), nullptr);
EXPECT_TRUE(IsEventSignaled(release_copy, ZX_EVENT_SIGNALED));
}
TEST_F(FlatlandTest, PresentsUpdateInCallOrder) {
Flatland flatland = CreateFlatland();
// Create an event to serve as the acquire fence for the first Present().
PresentArgs args1;
args1.acquire_fences = CreateEventArray(1);
auto acquire1_copy = CopyEvent(args1.acquire_fences[0]);
// Create an event to serve as a release fence.
args1.release_fences = CreateEventArray(1);
auto release1_copy = 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);
auto registered_presents = GetRegisteredPresents(flatland.GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 1ul);
EXPECT_EQ(GetUberStruct(flatland), nullptr);
EXPECT_FALSE(IsEventSignaled(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 = CreateEventArray(1);
auto acquire2_copy = CopyEvent(args2.acquire_fences[0]);
// Create an event to serve as a release fence.
args2.release_fences = CreateEventArray(1);
auto release2_copy = 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);
registered_presents = GetRegisteredPresents(flatland.GetRoot().GetInstanceId());
EXPECT_EQ(registered_presents.size(), 2ul);
EXPECT_EQ(GetUberStruct(flatland), nullptr);
EXPECT_FALSE(IsEventSignaled(release1_copy, ZX_EVENT_SIGNALED));
EXPECT_FALSE(IsEventSignaled(release2_copy, ZX_EVENT_SIGNALED));
// Signal the fence for the second Present(). Since the first one is not done, there should still
// be two 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(), 2ul);
EXPECT_EQ(GetUberStruct(flatland), nullptr);
EXPECT_FALSE(IsEventSignaled(release1_copy, ZX_EVENT_SIGNALED));
EXPECT_FALSE(IsEventSignaled(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);
EXPECT_NE(uber_struct, nullptr);
EXPECT_EQ(uber_struct->local_topology.size(), 2ul);
EXPECT_TRUE(IsEventSignaled(release1_copy, ZX_EVENT_SIGNALED));
EXPECT_TRUE(IsEventSignaled(release2_copy, ZX_EVENT_SIGNALED));
}
TEST_F(FlatlandTest, CreateAndReleaseTransformValidCases) {
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.ClearGraph();
flatland.CreateTransform(kId2);
flatland.CreateTransform(kId1);
PRESENT(flatland, true);
// Clear, create and release transforms, non-overlapping.
flatland.ClearGraph();
flatland.CreateTransform(kId1);
flatland.ReleaseTransform(kId1);
flatland.CreateTransform(kId2);
flatland.ReleaseTransform(kId2);
PRESENT(flatland, true);
// Clear, create and release transforms, nested.
flatland.ClearGraph();
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.ClearGraph();
flatland.CreateTransform(kId1);
PRESENT(flatland, true);
// Create and clear, overlapping, with multiple present calls.
flatland.ClearGraph();
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) {
Flatland flatland = CreateFlatland();
const TransformId kId1 = 1;
const TransformId kId2 = 2;
// Zero is not a valid transform id.
flatland.CreateTransform(0);
PRESENT(flatland, false);
flatland.ReleaseTransform(0);
PRESENT(flatland, false);
// Double creation is an error.
flatland.CreateTransform(kId1);
flatland.CreateTransform(kId1);
PRESENT(flatland, false);
// Releasing a non-existent transform is an error.
flatland.ReleaseTransform(kId2);
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, AddAndRemoveChildValidCases) {
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) {
Flatland flatland = CreateFlatland();
const TransformId kIdParent = 1;
const TransformId kIdChild = 2;
const TransformId kIdNotCreated = 3;
// Setup.
flatland.CreateTransform(kIdParent);
flatland.CreateTransform(kIdChild);
flatland.AddChild(kIdParent, kIdChild);
PRESENT(flatland, true);
// Zero is not a valid transform id.
flatland.AddChild(0, 0);
PRESENT(flatland, false);
flatland.AddChild(kIdParent, 0);
PRESENT(flatland, false);
flatland.AddChild(0, kIdChild);
PRESENT(flatland, false);
// Child does not exist.
flatland.AddChild(kIdParent, kIdNotCreated);
PRESENT(flatland, false);
flatland.RemoveChild(kIdParent, kIdNotCreated);
PRESENT(flatland, false);
// Parent does not exist.
flatland.AddChild(kIdNotCreated, kIdChild);
PRESENT(flatland, false);
flatland.RemoveChild(kIdNotCreated, kIdChild);
PRESENT(flatland, false);
// Child is already a child of parent.
flatland.AddChild(kIdParent, kIdChild);
PRESENT(flatland, false);
// Both nodes exist, but not in the correct relationship.
flatland.RemoveChild(kIdChild, kIdParent);
PRESENT(flatland, false);
}
// Test that Transforms can be children to multiple different parents.
TEST_F(FlatlandTest, MultichildUsecase) {
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 Present() fails if it detects a graph cycle.
TEST_F(FlatlandTest, CycleDetector) {
Flatland flatland = CreateFlatland();
const TransformId kId1 = 1;
const TransformId kId2 = 2;
const TransformId kId3 = 3;
const TransformId kId4 = 4;
// Create an immediate cycle.
{
flatland.CreateTransform(kId1);
flatland.AddChild(kId1, kId1);
PRESENT(flatland, false);
}
// Create a legal chain of depth one.
// Then, create a cycle of length 2.
{
flatland.ClearGraph();
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.
{
flatland.ClearGraph();
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.
{
flatland.ClearGraph();
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) {
Flatland flatland = CreateFlatland();
const TransformId kId1 = 1;
const TransformId kIdNotCreated = 2;
flatland.CreateTransform(kId1);
PRESENT(flatland, true);
// Even with no root transform, so clearing it is not an error.
flatland.SetRootTransform(0);
PRESENT(flatland, true);
// Setting the root to an unknown transform is an error.
flatland.SetRootTransform(kIdNotCreated);
PRESENT(flatland, false);
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);
EXPECT_EQ(uber_struct->local_topology.size(), 2ul);
flatland.SetRootTransform(kIdNotCreated);
PRESENT(flatland, false);
// The previous Present() fails, so we Present() again to ensure the UberStruct is updated,
// even though we expect no changes.
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland);
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);
uber_struct = GetUberStruct(flatland);
EXPECT_EQ(uber_struct->local_topology.size(), 2ul);
// 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);
}
TEST_F(FlatlandTest, SetTranslationErrorCases) {
Flatland flatland = CreateFlatland();
const TransformId kIdNotCreated = 1;
// Zero is not a valid transform ID.
flatland.SetTranslation(0, {1.f, 2.f});
PRESENT(flatland, false);
// Transform does not exist.
flatland.SetTranslation(kIdNotCreated, {1.f, 2.f});
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, SetOrientationErrorCases) {
Flatland flatland = CreateFlatland();
const TransformId kIdNotCreated = 1;
// Zero is not a valid transform ID.
flatland.SetOrientation(0, Orientation::CCW_90_DEGREES);
PRESENT(flatland, false);
// Transform does not exist.
flatland.SetOrientation(kIdNotCreated, Orientation::CCW_90_DEGREES);
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, SetScaleErrorCases) {
Flatland flatland = CreateFlatland();
const TransformId kIdNotCreated = 1;
// Zero is not a valid transform ID.
flatland.SetScale(0, {1.f, 2.f});
PRESENT(flatland, false);
// Transform does not exist.
flatland.SetScale(kIdNotCreated, {1.f, 2.f});
PRESENT(flatland, false);
}
// Test that changing geometric transform properties affects the local matrix of Transforms.
TEST_F(FlatlandTest, SetGeometricTransformProperties) {
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);
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);
EXPECT_TRUE(uber_struct->local_matrices.empty());
// Set up one property per transform.
flatland.SetTranslation(kId1, {1.f, 2.f});
flatland.SetScale(kId2, {2.f, 3.f});
PRESENT(flatland, true);
// The two handles should have the expected matrices.
uber_struct = GetUberStruct(flatland);
EXPECT_MATRIX(uber_struct, handle1, glm::translate(glm::mat3(), {1.f, 2.f}));
EXPECT_MATRIX(uber_struct, handle2, glm::scale(glm::mat3(), {2.f, 3.f}));
// Fill out the remaining properties on both transforms.
flatland.SetOrientation(kId1, Orientation::CCW_90_DEGREES);
flatland.SetScale(kId1, {4.f, 5.f});
flatland.SetTranslation(kId2, {6.f, 7.f});
flatland.SetOrientation(kId2, Orientation::CCW_270_DEGREES);
PRESENT(flatland, true);
// Verify the new properties were applied in the correct orders.
uber_struct = GetUberStruct(flatland);
glm::mat3 matrix1 = glm::mat3();
matrix1 = glm::translate(matrix1, {1.f, 2.f});
matrix1 = glm::rotate(matrix1, GetOrientationAngle(Orientation::CCW_90_DEGREES));
matrix1 = glm::scale(matrix1, {4.f, 5.f});
EXPECT_MATRIX(uber_struct, handle1, matrix1);
glm::mat3 matrix2 = glm::mat3();
matrix2 = glm::translate(matrix2, {6.f, 7.f});
matrix2 = glm::rotate(matrix2, GetOrientationAngle(Orientation::CCW_270_DEGREES));
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) {
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);
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.f, 2.f});
PRESENT(flatland, true);
// Only handle1 should have a local matrix.
uber_struct = GetUberStruct(flatland);
EXPECT_MATRIX(uber_struct, handle1, glm::translate(glm::mat3(), {1.f, 2.f}));
// Release kId1, but ensure its matrix stays around.
flatland.ReleaseTransform(kId1);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland);
EXPECT_MATRIX(uber_struct, handle1, glm::translate(glm::mat3(), {1.f, 2.f}));
// Clear kId1 as the root transform, which should clear the matrix.
flatland.SetRootTransform(0);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland);
EXPECT_TRUE(uber_struct->local_matrices.empty());
}
TEST_F(FlatlandTest, GraphLinkReplaceWithoutConnection) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<GraphLink> graph_link;
flatland.LinkToParent(std::move(child_token), graph_link.NewRequest());
PRESENT(flatland, true);
ContentLinkToken parent_token2;
GraphLinkToken child_token2;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token2.value, &child_token2.value));
fidl::InterfacePtr<GraphLink> graph_link2;
flatland.LinkToParent(std::move(child_token2), graph_link2.NewRequest());
RunLoopUntilIdle();
// Until Present() is called, the previous GraphLink is not unbound.
EXPECT_TRUE(graph_link.is_bound());
EXPECT_TRUE(graph_link2.is_bound());
PRESENT(flatland, true);
EXPECT_FALSE(graph_link.is_bound());
EXPECT_TRUE(graph_link2.is_bound());
}
TEST_F(FlatlandTest, GraphLinkReplaceWithConnection) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
fidl::InterfacePtr<GraphLink> graph_link2;
// Don't use the helper function for the second link to test when the previous links are closed.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
// Creating the new GraphLink doesn't invalidate either of the old links until Present() is
// called on the child.
child.LinkToParent(std::move(child_token), graph_link2.NewRequest());
RunLoopUntilIdle();
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
EXPECT_TRUE(graph_link2.is_bound());
// Present() replaces the original GraphLink, which also results in the invalidation of both ends
// of the original link.
PRESENT(child, true);
EXPECT_FALSE(content_link.is_bound());
EXPECT_FALSE(graph_link.is_bound());
EXPECT_TRUE(graph_link2.is_bound());
}
TEST_F(FlatlandTest, GraphLinkUnbindsOnParentDeath) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<GraphLink> graph_link;
flatland.LinkToParent(std::move(child_token), graph_link.NewRequest());
PRESENT(flatland, true);
parent_token.value.reset();
RunLoopUntilIdle();
EXPECT_FALSE(graph_link.is_bound());
}
TEST_F(FlatlandTest, GraphLinkUnbindsImmediatelyWithInvalidToken) {
Flatland flatland = CreateFlatland();
GraphLinkToken child_token;
fidl::InterfacePtr<GraphLink> graph_link;
flatland.LinkToParent(std::move(child_token), graph_link.NewRequest());
// The link will be unbound even before Present() is called.
RunLoopUntilIdle();
EXPECT_FALSE(graph_link.is_bound());
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, GraphUnlinkFailsWithoutLink) {
Flatland flatland = CreateFlatland();
flatland.UnlinkFromParent([](GraphLinkToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, GraphUnlinkReturnsOrphanedTokenOnParentDeath) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<GraphLink> graph_link;
flatland.LinkToParent(std::move(child_token), graph_link.NewRequest());
PRESENT(flatland, true);
// Killing the peer token does not prevent the instance from returning a valid token.
parent_token.value.reset();
RunLoopUntilIdle();
GraphLinkToken graph_token;
flatland.UnlinkFromParent(
[&graph_token](GraphLinkToken token) { graph_token = std::move(token); });
PRESENT(flatland, true);
EXPECT_TRUE(graph_token.value.is_valid());
// But trying to link with that token will immediately fail because it is already orphaned.
fidl::InterfacePtr<GraphLink> graph_link2;
flatland.LinkToParent(std::move(graph_token), graph_link2.NewRequest());
PRESENT(flatland, true);
EXPECT_FALSE(graph_link2.is_bound());
}
TEST_F(FlatlandTest, GraphUnlinkReturnsOriginalToken) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const zx_koid_t expected_koid = fsl::GetKoid(child_token.value.get());
fidl::InterfacePtr<GraphLink> graph_link;
flatland.LinkToParent(std::move(child_token), graph_link.NewRequest());
PRESENT(flatland, true);
GraphLinkToken graph_token;
flatland.UnlinkFromParent(
[&graph_token](GraphLinkToken token) { graph_token = std::move(token); });
RunLoopUntilIdle();
// Until Present() is called and the acquire fence is signaled, the previous GraphLink is not
// unbound.
EXPECT_TRUE(graph_link.is_bound());
EXPECT_FALSE(graph_token.value.is_valid());
PresentArgs args;
args.acquire_fences = CreateEventArray(1);
auto event_copy = CopyEvent(args.acquire_fences[0]);
PRESENT_WITH_ARGS(flatland, std::move(args), true);
EXPECT_TRUE(graph_link.is_bound());
EXPECT_FALSE(graph_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(graph_link.is_bound());
EXPECT_TRUE(graph_token.value.is_valid());
EXPECT_EQ(fsl::GetKoid(graph_token.value.get()), expected_koid);
}
TEST_F(FlatlandTest, ContentLinkUnbindsOnChildDeath) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kLinkId1, std::move(parent_token), std::move(properties),
content_link.NewRequest());
PRESENT(flatland, true);
child_token.value.reset();
RunLoopUntilIdle();
EXPECT_FALSE(content_link.is_bound());
}
TEST_F(FlatlandTest, ContentLinkUnbindsImmediatelyWithInvalidToken) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
flatland.CreateLink(kLinkId1, std::move(parent_token), {}, content_link.NewRequest());
// The link will be unbound even before Present() is called.
RunLoopUntilIdle();
EXPECT_FALSE(content_link.is_bound());
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ContentLinkFailsIdIsZero) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(0, std::move(parent_token), std::move(properties), content_link.NewRequest());
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ContentLinkFailsNoLogicalSize) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
flatland.CreateLink(0, std::move(parent_token), std::move(properties), content_link.NewRequest());
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ContentLinkFailsInvalidLogicalSize) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<ContentLink> content_link;
// The X value must be positive.
LinkProperties properties;
properties.set_logical_size({0.f, kDefaultSize});
flatland.CreateLink(0, std::move(parent_token), std::move(properties), content_link.NewRequest());
PRESENT(flatland, false);
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
// The Y value must be positive.
LinkProperties properties2;
properties2.set_logical_size({kDefaultSize, 0.f});
flatland.CreateLink(0, std::move(parent_token), std::move(properties2),
content_link.NewRequest());
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ContentLinkFailsIdCollision) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const ContentId kId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kId1, std::move(parent_token), std::move(properties),
content_link.NewRequest());
PRESENT(flatland, true);
ContentLinkToken parent_token2;
GraphLinkToken child_token2;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token2.value, &child_token2.value));
flatland.CreateLink(kId1, std::move(parent_token2), std::move(properties),
content_link.NewRequest());
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ClearGraphDelaysLinkDestructionUntilPresent) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
// Clearing the parent graph should not unbind the interfaces until Present() is called and the
// acquire fence is signaled.
parent.ClearGraph();
RunLoopUntilIdle();
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
PresentArgs args;
args.acquire_fences = CreateEventArray(1);
auto event_copy = CopyEvent(args.acquire_fences[0]);
PRESENT_WITH_ARGS(parent, std::move(args), true);
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
// Signal the acquire fence to unbind the links.
event_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _));
RunLoopUntilIdle();
EXPECT_FALSE(content_link.is_bound());
EXPECT_FALSE(graph_link.is_bound());
// Recreate the Link. The parent graph was cleared so we can reuse the LinkId.
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
// Clearing the child graph should not unbind the interfaces until Present() is called and the
// acquire fence is signaled.
child.ClearGraph();
RunLoopUntilIdle();
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
PresentArgs args2;
args2.acquire_fences = CreateEventArray(1);
event_copy = CopyEvent(args2.acquire_fences[0]);
PRESENT_WITH_ARGS(child, std::move(args2), true);
EXPECT_TRUE(content_link.is_bound());
EXPECT_TRUE(graph_link.is_bound());
// Signal the acquire fence to unbind the links.
event_copy.signal(0, ZX_EVENT_SIGNALED);
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _));
RunLoopUntilIdle();
EXPECT_FALSE(content_link.is_bound());
EXPECT_FALSE(graph_link.is_bound());
}
// 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) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
// Set up a link, but don't call Present() on either instance.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const ContentId kLinkId = 1;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({1.0f, 2.0f});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
content_link.NewRequest());
fidl::InterfacePtr<GraphLink> graph_link;
child.LinkToParent(std::move(child_token), graph_link.NewRequest());
// Request a layout update.
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(1.0f, info.logical_size().x);
EXPECT_EQ(2.0f, info.logical_size().y);
layout_updated = true;
});
// Without even presenting, the child is able to get the initial properties from the parent.
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
// 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) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
// Set up a link and attach it to the parent's root, but don't call Present() on either instance.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const TransformId kTransformId = 1;
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({1.0f, 2.0f});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kTransformId);
fidl::InterfacePtr<GraphLink> graph_link;
child.LinkToParent(std::move(child_token), graph_link.NewRequest());
// Request a status update.
bool status_updated = false;
graph_link->GetStatus([&](GraphLinkStatus status) {
EXPECT_EQ(status, GraphLinkStatus::DISCONNECTED_FROM_DISPLAY);
status_updated = true;
});
// The child begins disconnected from the display.
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
// The GraphLinkStatus will update when both the parent and child Present().
status_updated = false;
graph_link->GetStatus([&](GraphLinkStatus status) {
EXPECT_EQ(status, GraphLinkStatus::CONNECTED_TO_DISPLAY);
status_updated = true;
});
// The parent presents first, no update.
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
EXPECT_FALSE(status_updated);
// The child presents second and the status updates.
PRESENT(child, true);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
}
// 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) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
// Set up a link and attach it to the parent's root, but don't call Present() on either instance.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const TransformId kTransformId = 1;
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({1.0f, 2.0f});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kTransformId);
fidl::InterfacePtr<GraphLink> graph_link;
child.LinkToParent(std::move(child_token), graph_link.NewRequest());
// Request a status update.
bool status_updated = false;
graph_link->GetStatus([&](GraphLinkStatus status) {
EXPECT_EQ(status, GraphLinkStatus::DISCONNECTED_FROM_DISPLAY);
status_updated = true;
});
// The child begins disconnected from the display.
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
// The GraphLinkStatus will update when both the parent and child Present().
status_updated = false;
graph_link->GetStatus([&](GraphLinkStatus status) {
EXPECT_EQ(status, GraphLinkStatus::CONNECTED_TO_DISPLAY);
status_updated = true;
});
// The child presents first, no update.
PRESENT(child, true);
UpdateLinks(parent.GetRoot());
EXPECT_FALSE(status_updated);
// The parent presents second and the status updates.
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
}
// 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) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
// Set up a link and attach it to the parent's root, but don't call Present() on either instance.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const TransformId kTransformId = 1;
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({1.0f, 2.0f});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kTransformId);
fidl::InterfacePtr<GraphLink> graph_link;
child.LinkToParent(std::move(child_token), graph_link.NewRequest());
// The GraphLinkStatus will update when both the parent and child Present().
bool status_updated = false;
graph_link->GetStatus([&](GraphLinkStatus status) {
EXPECT_EQ(status, GraphLinkStatus::CONNECTED_TO_DISPLAY);
status_updated = true;
});
PRESENT(child, true);
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
// The GraphLinkStatus will update again if the parent removes the child link from its topology.
status_updated = false;
graph_link->GetStatus([&](GraphLinkStatus status) {
EXPECT_EQ(status, GraphLinkStatus::DISCONNECTED_FROM_DISPLAY);
status_updated = true;
});
parent.SetContentOnTransform(0, kTransformId);
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
}
// 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) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const ContentId kLinkId = 1;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({1.0f, 2.0f});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
content_link.NewRequest());
fidl::InterfacePtr<GraphLink> graph_link;
child.LinkToParent(std::move(child_token), graph_link.NewRequest());
bool status_updated = false;
content_link->GetStatus([&](ContentLinkStatus status) {
ASSERT_EQ(ContentLinkStatus::CONTENT_HAS_PRESENTED, status);
status_updated = true;
});
// The content link status changes as soon as the child presents - the parent does not have to
// present.
EXPECT_FALSE(status_updated);
PRESENT(child, true);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(status_updated);
}
TEST_F(FlatlandTest, LayoutOnlyUpdatesChildrenInGlobalTopology) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const TransformId kTransformId = 1;
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId, &content_link, &graph_link);
UpdateLinks(parent.GetRoot());
// Confirm that the initial logical size is available immediately.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(kDefaultSize, info.logical_size().x);
EXPECT_EQ(kDefaultSize, info.logical_size().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
// Set the logical size to something new.
{
LinkProperties properties;
properties.set_logical_size({2.0f, 3.0f});
parent.SetLinkProperties(kLinkId, std::move(properties));
PRESENT(parent, true);
}
// Confirm that no update is triggered since the child is not in the global topology.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) { layout_updated = true; });
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_FALSE(layout_updated);
}
// Attach the child to the global topology.
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
parent.SetContentOnTransform(kLinkId, kTransformId);
PRESENT(parent, true);
// Confirm that the new logical size is accessible.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(2.0f, info.logical_size().x);
EXPECT_EQ(3.0f, info.logical_size().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
}
TEST_F(FlatlandTest, SetLinkPropertiesDefaultBehavior) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const TransformId kTransformId = 1;
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId, &content_link, &graph_link);
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
parent.SetContentOnTransform(kLinkId, kTransformId);
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
// Confirm that the initial layout is the default.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(kDefaultSize, info.logical_size().x);
EXPECT_EQ(kDefaultSize, info.logical_size().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
// Set the logical size to something new.
{
LinkProperties properties;
properties.set_logical_size({2.0f, 3.0f});
parent.SetLinkProperties(kLinkId, std::move(properties));
PRESENT(parent, true);
}
// Confirm that the new logical size is accessible.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(2.0f, info.logical_size().x);
EXPECT_EQ(3.0f, info.logical_size().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
// Set link properties using a properties object with an unset size field.
{
LinkProperties default_properties;
parent.SetLinkProperties(kLinkId, std::move(default_properties));
PRESENT(parent, true);
}
// Confirm that no update has been triggered.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) { layout_updated = true; });
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_FALSE(layout_updated);
}
}
TEST_F(FlatlandTest, SetLinkPropertiesMultisetBehavior) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const TransformId kTransformId = 1;
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId, &content_link, &graph_link);
// Our initial layout (from link creation) should be the default size.
{
int num_updates = 0;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(kDefaultSize, info.logical_size().x);
EXPECT_EQ(kDefaultSize, info.logical_size().y);
++num_updates;
});
EXPECT_EQ(0, num_updates);
UpdateLinks(parent.GetRoot());
EXPECT_EQ(1, num_updates);
}
// Create a full chain of transforms from parent root to child root.
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
parent.SetContentOnTransform(kLinkId, kTransformId);
PRESENT(parent, true);
const float kInitialSize = 100.0f;
// Set the logical size to something new multiple times.
for (int i = 10; i >= 0; --i) {
LinkProperties properties;
properties.set_logical_size({kInitialSize + i + 1.0f, kInitialSize + i + 1.0f});
parent.SetLinkProperties(kLinkId, std::move(properties));
LinkProperties properties2;
properties2.set_logical_size({kInitialSize + i, kInitialSize + i});
parent.SetLinkProperties(kLinkId, std::move(properties2));
PRESENT(parent, true);
}
// Confirm that the callback is fired once, and that it has the most up-to-date data.
{
int num_updates = 0;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(kInitialSize, info.logical_size().x);
EXPECT_EQ(kInitialSize, info.logical_size().y);
++num_updates;
});
EXPECT_EQ(0, num_updates);
UpdateLinks(parent.GetRoot());
EXPECT_EQ(1, num_updates);
}
const float kNewSize = 50.0f;
// Confirm that calling GetLayout again results in a hung get.
int num_updates = 0;
graph_link->GetLayout([&](LayoutInfo info) {
// When we receive the new layout information, confirm that we receive the last update in the
// batch.
EXPECT_EQ(kNewSize, info.logical_size().x);
EXPECT_EQ(kNewSize, info.logical_size().y);
++num_updates;
});
EXPECT_EQ(0, num_updates);
UpdateLinks(parent.GetRoot());
EXPECT_EQ(0, num_updates);
// Update the properties twice, once with the old value, once with the new value.
{
LinkProperties properties;
properties.set_logical_size({kInitialSize, kInitialSize});
parent.SetLinkProperties(kLinkId, std::move(properties));
LinkProperties properties2;
properties2.set_logical_size({kNewSize, kNewSize});
parent.SetLinkProperties(kLinkId, std::move(properties2));
PRESENT(parent, true);
}
// Confirm that we receive the update.
EXPECT_EQ(0, num_updates);
UpdateLinks(parent.GetRoot());
EXPECT_EQ(1, num_updates);
}
TEST_F(FlatlandTest, SetLinkPropertiesOnMultipleChildren) {
const int kNumChildren = 3;
const TransformId kRootTransform = 1;
const TransformId kTransformIds[kNumChildren] = {2, 3, 4};
const ContentId kLinkIds[kNumChildren] = {5, 6, 7};
Flatland parent = CreateFlatland();
Flatland children[kNumChildren] = {CreateFlatland(), CreateFlatland(), CreateFlatland()};
fidl::InterfacePtr<ContentLink> content_link[kNumChildren];
fidl::InterfacePtr<GraphLink> graph_link[kNumChildren];
parent.CreateTransform(kRootTransform);
parent.SetRootTransform(kRootTransform);
for (int i = 0; i < kNumChildren; ++i) {
parent.CreateTransform(kTransformIds[i]);
parent.AddChild(kRootTransform, kTransformIds[i]);
CreateLink(&parent, &children[i], kLinkIds[i], &content_link[i], &graph_link[i]);
parent.SetContentOnTransform(kLinkIds[i], kTransformIds[i]);
}
UpdateLinks(parent.GetRoot());
const float kDefaultSize = 1.0f;
// Confirm that all children are at the default value
for (int i = 0; i < kNumChildren; ++i) {
bool layout_updated = false;
graph_link[i]->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(kDefaultSize, info.logical_size().x);
EXPECT_EQ(kDefaultSize, info.logical_size().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
// Resize the content on all children.
for (auto id : kLinkIds) {
LinkProperties properties;
properties.set_logical_size({static_cast<float>(id), id * 2.0f});
parent.SetLinkProperties(id, std::move(properties));
}
PRESENT(parent, true);
for (int i = 0; i < kNumChildren; ++i) {
bool layout_updated = false;
graph_link[i]->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(kLinkIds[i], info.logical_size().x);
EXPECT_EQ(kLinkIds[i] * 2.0f, info.logical_size().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
}
TEST_F(FlatlandTest, DisplayPixelScaleAffectsPixelScale) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const TransformId kTransformId = 1;
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId, &content_link, &graph_link);
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
parent.SetContentOnTransform(kLinkId, kTransformId);
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
// Change the display pixel scale.
const glm::vec2 new_display_pixel_scale = {0.1f, 0.2f};
SetDisplayPixelScale(new_display_pixel_scale);
// Call and ignore GetLayout() to guarantee the next call hangs.
graph_link->GetLayout([&](LayoutInfo info) {});
// Confirm that the new pixel scale is (.1, .2).
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_EQ(new_display_pixel_scale.x, info.pixel_scale().x);
EXPECT_EQ(new_display_pixel_scale.y, info.pixel_scale().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
}
TEST_F(FlatlandTest, LinkSizesAffectPixelScale) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const TransformId kTransformId = 1;
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId, &content_link, &graph_link);
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
parent.SetContentOnTransform(kLinkId, kTransformId);
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
// Change the link size and logical size of the link.
const Vec2 kNewLinkSize = {2.f, 3.f};
parent.SetLinkSize(kLinkId, kNewLinkSize);
const Vec2 kNewLogicalSize = {5.f, 7.f};
{
LinkProperties properties;
properties.set_logical_size(kNewLogicalSize);
parent.SetLinkProperties(kLinkId, std::move(properties));
}
PRESENT(parent, true);
// Call and ignore GetLayout() to guarantee the next call hangs.
graph_link->GetLayout([&](LayoutInfo info) {});
// Confirm that the new pixel scale is (2 / 5, 3 / 7).
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_FLOAT_EQ(kNewLinkSize.x / kNewLogicalSize.x, info.pixel_scale().x);
EXPECT_FLOAT_EQ(kNewLinkSize.y / kNewLogicalSize.y, info.pixel_scale().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
}
TEST_F(FlatlandTest, GeometricAttributesAffectPixelScale) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const TransformId kTransformId = 1;
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId, &content_link, &graph_link);
parent.CreateTransform(kTransformId);
parent.SetRootTransform(kTransformId);
parent.SetContentOnTransform(kLinkId, kTransformId);
PRESENT(parent, true);
UpdateLinks(parent.GetRoot());
// Set a scale on the parent transform.
const Vec2 scale = {2.f, 3.f};
parent.SetScale(kTransformId, scale);
PRESENT(parent, true);
// Call and ignore GetLayout() to guarantee the next call hangs.
graph_link->GetLayout([&](LayoutInfo info) {});
// Confirm that the new pixel scale is (2, 3).
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_FLOAT_EQ(scale.x, info.pixel_scale().x);
EXPECT_FLOAT_EQ(scale.y, info.pixel_scale().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
// Set a negative scale, but confirm that pixel scale is still positive.
parent.SetScale(kTransformId, {-scale.x, -scale.y});
PRESENT(parent, true);
// Call and ignore GetLayout() to guarantee the next call hangs.
graph_link->GetLayout([&](LayoutInfo info) {});
// Pixel scale is still (2, 3), so nothing changes.
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) { layout_updated = true; });
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_FALSE(layout_updated);
}
// Set a rotation on the parent transform.
parent.SetOrientation(kTransformId, Orientation::CCW_90_DEGREES);
PRESENT(parent, true);
// Call and ignore GetLayout() to guarantee the next call hangs.
graph_link->GetLayout([&](LayoutInfo info) {});
// Confirm that this flips the new pixel scale to (3, 2).
{
bool layout_updated = false;
graph_link->GetLayout([&](LayoutInfo info) {
EXPECT_FLOAT_EQ(scale.y, info.pixel_scale().x);
EXPECT_FLOAT_EQ(scale.x, info.pixel_scale().y);
layout_updated = true;
});
EXPECT_FALSE(layout_updated);
UpdateLinks(parent.GetRoot());
EXPECT_TRUE(layout_updated);
}
}
TEST_F(FlatlandTest, SetLinkOnTransformErrorCases) {
Flatland flatland = CreateFlatland();
// Setup.
const TransformId kId1 = 1;
const TransformId kId2 = 2;
flatland.CreateTransform(kId1);
const ContentId kLinkId1 = 1;
const ContentId kLinkId2 = 2;
fidl::InterfacePtr<ContentLink> content_link;
// Creating a link with an empty property object is an error. Logical size must be provided at
// creation time.
{
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
LinkProperties empty_properties;
flatland.CreateLink(kLinkId1, std::move(parent_token), std::move(empty_properties),
content_link.NewRequest());
PRESENT(flatland, false);
}
// We have to recreate our tokens to get a valid link object.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kLinkId1, std::move(parent_token), std::move(properties),
content_link.NewRequest());
PRESENT(flatland, true);
// Zero is not a valid transform_id.
flatland.SetContentOnTransform(kLinkId1, 0);
PRESENT(flatland, false);
// Setting a valid link on an ivnalid transform is not valid.
flatland.SetContentOnTransform(kLinkId1, kId2);
PRESENT(flatland, false);
// Setting an invalid link on a valid transform is not valid.
flatland.SetContentOnTransform(kLinkId2, kId1);
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ReleaseLinkErrorCases) {
Flatland flatland = CreateFlatland();
// Zero is not a valid link_id.
flatland.ReleaseLink(0, [](ContentLinkToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
// Using a link_id that does not exist is not valid.
const ContentId kLinkId1 = 1;
flatland.ReleaseLink(kLinkId1, [](ContentLinkToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
// ContentId is not a Link.
const ContentId kImageId = 2;
const BufferCollectionId kBufferCollectionId = 3;
ImageProperties properties;
properties.set_width(100);
properties.set_height(200);
CreateImage(&flatland, kImageId, kBufferCollectionId, std::move(properties));
flatland.ReleaseLink(kImageId, [](ContentLinkToken token) { EXPECT_TRUE(false); });
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ReleaseLinkReturnsOriginalToken) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const zx_koid_t expected_koid = fsl::GetKoid(parent_token.value.get());
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kLinkId1, std::move(parent_token), std::move(properties),
content_link.NewRequest());
PRESENT(flatland, true);
ContentLinkToken content_token;
flatland.ReleaseLink(
kLinkId1, [&content_token](ContentLinkToken token) { content_token = std::move(token); });
RunLoopUntilIdle();
// Until Present() is called and the acquire fence is signaled, the previous ContentLink is not
// unbound.
EXPECT_TRUE(content_link.is_bound());
EXPECT_FALSE(content_token.value.is_valid());
PresentArgs args;
args.acquire_fences = CreateEventArray(1);
auto event_copy = CopyEvent(args.acquire_fences[0]);
PRESENT_WITH_ARGS(flatland, std::move(args), true);
EXPECT_TRUE(content_link.is_bound());
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(content_link.is_bound());
EXPECT_TRUE(content_token.value.is_valid());
EXPECT_EQ(fsl::GetKoid(content_token.value.get()), expected_koid);
}
TEST_F(FlatlandTest, ReleaseLinkReturnsOrphanedTokenOnChildDeath) {
Flatland flatland = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kLinkId1, std::move(parent_token), std::move(properties),
content_link.NewRequest());
PRESENT(flatland, true);
// Killing the peer token does not prevent the instance from returning a valid token.
child_token.value.reset();
RunLoopUntilIdle();
ContentLinkToken content_token;
flatland.ReleaseLink(
kLinkId1, [&content_token](ContentLinkToken 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;
fidl::InterfacePtr<ContentLink> content_link2;
flatland.CreateLink(kLinkId2, std::move(content_token), std::move(properties),
content_link2.NewRequest());
PRESENT(flatland, true);
EXPECT_FALSE(content_link2.is_bound());
}
TEST_F(FlatlandTest, CreateLinkPresentedBeforeLinkToParent) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::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;
fidl::InterfacePtr<ContentLink> parent_content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
parent_content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kId1);
PRESENT(parent, true);
// Link the child to the parent.
fidl::InterfacePtr<GraphLink> child_graph_link;
child.LinkToParent(std::move(child_token), child_graph_link.NewRequest());
// 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, LinkToParentPresentedBeforeCreateLink) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
// Link the child to the parent
fidl::InterfacePtr<GraphLink> child_graph_link;
child.LinkToParent(std::move(child_token), child_graph_link.NewRequest());
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;
fidl::InterfacePtr<ContentLink> parent_content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
parent_content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kId1);
// 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) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::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;
fidl::InterfacePtr<ContentLink> parent_content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
parent_content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kId1);
// Link the child to the parent.
fidl::InterfacePtr<GraphLink> child_graph_link;
child.LinkToParent(std::move(child_token), child_graph_link.NewRequest());
// 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, ClearChildLink) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::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;
fidl::InterfacePtr<ContentLink> parent_content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
parent.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
parent_content_link.NewRequest());
parent.SetContentOnTransform(kLinkId, kId1);
fidl::InterfacePtr<GraphLink> child_graph_link;
child.LinkToParent(std::move(child_token), child_graph_link.NewRequest());
PRESENT(parent, true);
PRESENT(child, true);
EXPECT_TRUE(IsDescendantOf(parent.GetRoot(), child.GetRoot()));
// Reset the child link using zero as the link id.
parent.SetContentOnTransform(0, kId1);
PRESENT(parent, true);
EXPECT_FALSE(IsDescendantOf(parent.GetRoot(), child.GetRoot()));
}
TEST_F(FlatlandTest, RelinkUnlinkedParentSameToken) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
RunLoopUntilIdle();
const TransformId kId1 = 1;
parent.CreateTransform(kId1);
parent.SetRootTransform(kId1);
parent.SetContentOnTransform(kId1, kLinkId1);
PRESENT(parent, true);
EXPECT_TRUE(IsDescendantOf(parent.GetRoot(), child.GetRoot()));
GraphLinkToken graph_token;
child.UnlinkFromParent([&graph_token](GraphLinkToken token) { graph_token = std::move(token); });
PRESENT(child, true);
EXPECT_FALSE(IsDescendantOf(parent.GetRoot(), child.GetRoot()));
// The same token can be used to link a different instance.
Flatland child2 = CreateFlatland();
child2.LinkToParent(std::move(graph_token), graph_link.NewRequest());
PRESENT(child2, true);
EXPECT_TRUE(IsDescendantOf(parent.GetRoot(), child2.GetRoot()));
// The old instance is not re-linked.
EXPECT_FALSE(IsDescendantOf(parent.GetRoot(), child.GetRoot()));
}
TEST_F(FlatlandTest, RecreateReleasedLinkSameToken) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
RunLoopUntilIdle();
const TransformId kId1 = 1;
parent.CreateTransform(kId1);
parent.SetRootTransform(kId1);
parent.SetContentOnTransform(kId1, kLinkId1);
PRESENT(parent, true);
EXPECT_TRUE(IsDescendantOf(parent.GetRoot(), child.GetRoot()));
ContentLinkToken content_token;
parent.ReleaseLink(
kLinkId1, [&content_token](ContentLinkToken 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.
Flatland parent2 = CreateFlatland();
const TransformId kId2 = 2;
parent2.CreateTransform(kId2);
parent2.SetRootTransform(kId2);
const ContentId kLinkId2 = 2;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
parent2.CreateLink(kLinkId2, std::move(content_token), std::move(properties),
content_link.NewRequest());
parent2.SetContentOnTransform(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()));
}
TEST_F(FlatlandTest, SetLinkSizeErrorCases) {
Flatland flatland = CreateFlatland();
const ContentId kIdNotCreated = 1;
// Zero is not a valid transform ID.
flatland.SetLinkSize(0, {1.f, 2.f});
PRESENT(flatland, false);
// Size contains non-positive components.
flatland.SetLinkSize(0, {-1.f, 2.f});
PRESENT(flatland, false);
flatland.SetLinkSize(0, {1.f, 0.f});
PRESENT(flatland, false);
// Link does not exist.
flatland.SetLinkSize(kIdNotCreated, {1.f, 2.f});
PRESENT(flatland, false);
// ContentId is not a Link.
const ContentId kImageId = 2;
const BufferCollectionId kBufferCollectionId = 3;
ImageProperties properties;
properties.set_width(100);
properties.set_height(200);
CreateImage(&flatland, kImageId, kBufferCollectionId, std::move(properties));
flatland.SetLinkSize(kImageId, {1.f, 2.f});
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, LinkSizeRatiosCreateScaleMatrix) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
const TransformId kId1 = 1;
parent.CreateTransform(kId1);
parent.SetRootTransform(kId1);
parent.SetContentOnTransform(kLinkId1, kId1);
PRESENT(parent, true);
const auto maybe_link_handle = parent.GetContentHandle(kLinkId1);
ASSERT_TRUE(maybe_link_handle.has_value());
const auto link_handle = maybe_link_handle.value();
// The default size is the same as the logical size, so the link handle won't have a matrix.
auto uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, glm::mat3());
// Change the link size to half the width and a quarter the height.
const float kNewLinkWidth = 0.5f * kDefaultSize;
const float kNewLinkHeight = 0.25f * kDefaultSize;
parent.SetLinkSize(kLinkId1, {kNewLinkWidth, kNewLinkHeight});
PRESENT(parent, true);
// This should change the expected matrix to apply the same scales.
const glm::mat3 expected_scale_matrix = glm::scale(glm::mat3(), {kNewLinkWidth, kNewLinkHeight});
uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, expected_scale_matrix);
// Changing the logical size to the same values returns the matrix to the identity matrix.
LinkProperties properties;
properties.set_logical_size({kNewLinkWidth, kNewLinkHeight});
parent.SetLinkProperties(kLinkId1, std::move(properties));
PRESENT(parent, true);
uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, glm::mat3());
// Change the logical size back to the default size.
LinkProperties properties2;
properties2.set_logical_size({kDefaultSize, kDefaultSize});
parent.SetLinkProperties(kLinkId1, std::move(properties2));
PRESENT(parent, true);
// This should change the expected matrix back to applying the scales.
uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, expected_scale_matrix);
}
TEST_F(FlatlandTest, EmptyLogicalSizePreservesOldSize) {
Flatland parent = CreateFlatland();
Flatland child = CreateFlatland();
const ContentId kLinkId1 = 1;
fidl::InterfacePtr<ContentLink> content_link;
fidl::InterfacePtr<GraphLink> graph_link;
CreateLink(&parent, &child, kLinkId1, &content_link, &graph_link);
const TransformId kId1 = 1;
parent.CreateTransform(kId1);
parent.SetRootTransform(kId1);
parent.SetContentOnTransform(kLinkId1, kId1);
PRESENT(parent, true);
const auto maybe_link_handle = parent.GetContentHandle(kLinkId1);
ASSERT_TRUE(maybe_link_handle.has_value());
const auto link_handle = maybe_link_handle.value();
// Set the link size and logical size to new values
const float kNewLinkWidth = 2.f * kDefaultSize;
const float kNewLinkHeight = 3.f * kDefaultSize;
parent.SetLinkSize(kLinkId1, {kNewLinkWidth, kNewLinkHeight});
const float kNewLinkLogicalWidth = 5.f * kDefaultSize;
const float kNewLinkLogicalHeight = 7.f * kDefaultSize;
LinkProperties properties;
properties.set_logical_size({kNewLinkLogicalWidth, kNewLinkLogicalHeight});
parent.SetLinkProperties(kLinkId1, std::move(properties));
PRESENT(parent, true);
// This should result in an expected matrix that applies the ratio of the scales.
glm::mat3 expected_scale_matrix = glm::scale(
glm::mat3(), {kNewLinkWidth / kNewLinkLogicalWidth, kNewLinkHeight / kNewLinkLogicalHeight});
auto uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, expected_scale_matrix);
// Setting a new LinkProperties with no logical size shouldn't change the matrix.
LinkProperties properties2;
parent.SetLinkProperties(kLinkId1, std::move(properties2));
PRESENT(parent, true);
uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, expected_scale_matrix);
// But it should still preserve the old logical size so that a subsequent link size update uses
// the old logical size.
const float kNewLinkWidth2 = 11.f * kDefaultSize;
const float kNewLinkHeight2 = 13.f * kDefaultSize;
parent.SetLinkSize(kLinkId1, {kNewLinkWidth2, kNewLinkHeight2});
PRESENT(parent, true);
// This should result in an expected matrix that applies the ratio of the scales.
expected_scale_matrix = glm::scale(glm::mat3(), {kNewLinkWidth2 / kNewLinkLogicalWidth,
kNewLinkHeight2 / kNewLinkLogicalHeight});
uber_struct = GetUberStruct(parent);
EXPECT_MATRIX(uber_struct, link_handle, expected_scale_matrix);
}
TEST_F(FlatlandTest, RegisterBufferCollectionErrorCases) {
Flatland flatland = CreateFlatland();
// Zero is not a valid buffer collection ID.
{
flatland.RegisterBufferCollection(0, CreateToken());
PRESENT(flatland, false);
}
// Passing an uninitiated token is not valid.
{
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token;
flatland.RegisterBufferCollection(0, std::move(token));
PRESENT(flatland, false);
}
// Passing a token whose channel(s) have closed or gone out of scope is
// also not valid.
{
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token;
{
zx::channel local;
zx::channel remote;
zx::channel::create(0, &local, &remote);
token = fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken>(std::move(remote));
}
flatland.RegisterBufferCollection(0, std::move(token));
PRESENT(flatland, false);
}
// The buffer importer call can fail.
{
// Mock the importer call to fail.
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(Return(false));
flatland.RegisterBufferCollection(1, CreateToken());
PRESENT(flatland, false);
}
// Two buffer collections cannot use the same ID.
{
const BufferCollectionId kId = 2;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(Return(true));
flatland.RegisterBufferCollection(kId, CreateToken());
PRESENT(flatland, true);
flatland.RegisterBufferCollection(kId, CreateToken());
PRESENT(flatland, false);
}
}
// Tests that Flatland passes the Sysmem token to the importer even if the client has not called
// Present(). This is necessary since the client may block on buffers being allocated before
// presenting.
TEST_F(FlatlandTest, BufferImporterGetsSysmemTokenBeforePresent) {
Flatland flatland = CreateFlatland();
// Register a buffer collection and expect the mock buffer importer call, even without presenting.
const BufferCollectionId kId = 2;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _)).Times(1);
flatland.RegisterBufferCollection(kId, CreateToken());
}
TEST_F(FlatlandTest, CreateImageErrorCases) {
Flatland flatland = CreateFlatland();
// Default image properties.
const uint32_t kDefaultVmoIndex = 1;
const uint32_t kDefaultWidth = 100;
const uint32_t kDefaultHeight = 1000;
// Setup a valid buffer collection.
const BufferCollectionId kBufferCollectionId = 1;
sysmem_util::GlobalBufferCollectionId global_collection_id;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id = collection_id;
return true;
}));
flatland.RegisterBufferCollection(kBufferCollectionId, CreateToken());
PRESENT(flatland, true);
// Zero is not a valid image ID.
{
ImageProperties properties;
flatland.CreateImage(0, kBufferCollectionId, kDefaultVmoIndex, std::move(properties));
PRESENT(flatland, false);
}
// The buffer collection ID must also be valid.
{
ImageProperties properties;
flatland.CreateImage(1, 0, kDefaultVmoIndex, std::move(properties));
PRESENT(flatland, false);
}
// The buffer collection can fail to create an image.
{
ImageProperties properties;
flatland.CreateImage(1, kBufferCollectionId, 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.
{
const ContentId kId = 100;
ImageProperties properties;
properties.set_width(kDefaultWidth);
properties.set_height(kDefaultHeight);
EXPECT_CALL(*mock_buffer_collection_importer_, ImportImage(_)).WillOnce(Return(false));
flatland.CreateImage(kId, kBufferCollectionId, kDefaultVmoIndex, std::move(properties));
PRESENT(flatland, false);
}
// Two images cannot have the same ID.
const ContentId kId = 1;
{
ImageProperties properties;
properties.set_width(kDefaultWidth);
properties.set_height(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_, ImportImage(_)).WillOnce(Return(true));
flatland.CreateImage(kId, kBufferCollectionId, kDefaultVmoIndex, std::move(properties));
PRESENT(flatland, true);
}
{
ImageProperties properties;
properties.set_width(kDefaultWidth);
properties.set_height(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_, ImportImage(_)).Times(0);
flatland.CreateImage(kId, kBufferCollectionId, kDefaultVmoIndex, std::move(properties));
PRESENT(flatland, false);
}
// A Link id cannot be used for an image.
const ContentId kLinkId = 2;
{
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties link_properties;
link_properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kLinkId, std::move(parent_token), std::move(link_properties),
content_link.NewRequest());
PRESENT(flatland, true);
ImageProperties image_properties;
image_properties.set_width(kDefaultWidth);
image_properties.set_height(kDefaultHeight);
flatland.CreateImage(kLinkId, kBufferCollectionId, kDefaultVmoIndex,
std::move(image_properties));
PRESENT(flatland, false);
}
}
TEST_F(FlatlandTest, SetContentOnTransformErrorCases) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
const uint32_t kWidth = 100;
const uint32_t kHeight = 200;
ImageProperties properties;
properties.set_width(kWidth);
properties.set_height(kHeight);
CreateImage(&flatland, kImageId, kBufferCollectionId, std::move(properties));
// Create a transform.
const TransformId kTransformId = 1;
flatland.CreateTransform(kTransformId);
PRESENT(flatland, true);
// Zero is not a valid transform.
flatland.SetContentOnTransform(kImageId, 0);
PRESENT(flatland, false);
// The transform must exist.
flatland.SetContentOnTransform(kImageId, 2);
PRESENT(flatland, false);
// The image must exist.
flatland.SetContentOnTransform(2, kTransformId);
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, ClearContentOnTransform) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
ImageProperties properties;
properties.set_width(100);
properties.set_height(200);
auto global_collection_id =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
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);
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.SetContentOnTransform(0, kTransformId);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland);
for (const auto& entry : uber_struct->local_topology) {
EXPECT_NE(entry.handle, image_handle);
}
}
TEST_F(FlatlandTest, TopologyVisitsContentBeforeChildren) {
Flatland flatland = CreateFlatland();
// Setup two valid images.
const ContentId kImageId1 = 1;
const BufferCollectionId kBufferCollectionId1 = 1;
ImageProperties properties1;
properties1.set_width(100);
properties1.set_height(200);
CreateImage(&flatland, kImageId1, kBufferCollectionId1, 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;
const BufferCollectionId kBufferCollectionId2 = 2;
ImageProperties properties2;
properties2.set_width(300);
properties2.set_height(400);
CreateImage(&flatland, kImageId2, kBufferCollectionId2, 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.SetContentOnTransform(kImageId1, kTransformId1);
flatland.SetContentOnTransform(kImageId2, kTransformId2);
flatland.SetContentOnTransform(kImageId1, kTransformId3);
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);
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.SetContentOnTransform(0, kTransformId1);
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);
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());
}
TEST_F(FlatlandTest, DeregisterBufferCollectionErrorCases) {
Flatland flatland = CreateFlatland();
// Zero is not a buffer collection ID.
flatland.DeregisterBufferCollection(0);
PRESENT(flatland, false);
// The buffer collection ID must exist.
flatland.DeregisterBufferCollection(1);
PRESENT(flatland, false);
// A buffer collection cannot be deregistered twice.
sysmem_util::GlobalBufferCollectionId global_collection_id;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id = collection_id;
return true;
}));
const BufferCollectionId kBufferCollectionId = 3;
flatland.RegisterBufferCollection(kBufferCollectionId, CreateToken());
PRESENT(flatland, true);
// The PRESENT macro triggers release fences, which in turn triggers the buffer importer call.
flatland.DeregisterBufferCollection(kBufferCollectionId);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id))
.Times(1);
PRESENT(flatland, true);
flatland.DeregisterBufferCollection(kBufferCollectionId);
PRESENT(flatland, false);
}
TEST_F(FlatlandTest, DeregisterMultipleBufferCollectionsSameEvent) {
Flatland flatland = CreateFlatland();
// Register the first buffer collection.
sysmem_util::GlobalBufferCollectionId global_collection_id_1;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id_1](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id_1 = collection_id;
return true;
}));
const BufferCollectionId kBufferCollectionId1 = 2;
{ flatland.RegisterBufferCollection(kBufferCollectionId1, CreateToken()); }
// Register the second buffer collection.
sysmem_util::GlobalBufferCollectionId global_collection_id_2;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id_2](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id_2 = collection_id;
return true;
}));
const BufferCollectionId kBufferCollectionId2 = 4;
{ flatland.RegisterBufferCollection(kBufferCollectionId2, CreateToken()); }
PRESENT(flatland, true);
// Deregister both buffer collections so they're both waiting on the same release fence.
flatland.DeregisterBufferCollection(kBufferCollectionId1);
flatland.DeregisterBufferCollection(kBufferCollectionId2);
// Skip session updates to test that release fences are what trigger the buffer importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_1))
.Times(0);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_2))
.Times(0);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
// Signal the release fence.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_1))
.Times(1);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_2))
.Times(1);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, DeregisteredBufferCollectionIdCanBeReused) {
Flatland flatland = CreateFlatland();
// Create a valid BufferCollectionId.
sysmem_util::GlobalBufferCollectionId global_collection_id_1;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id_1](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id_1 = collection_id;
return true;
}));
const BufferCollectionId kBufferCollectionId = 2;
{
flatland.RegisterBufferCollection(kBufferCollectionId, CreateToken());
PRESENT(flatland, true);
}
// Deregister it, but don't signal the release fence yet.
flatland.DeregisterBufferCollection(kBufferCollectionId);
{
// Skip session updates to test that release fences are what trigger the buffer importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_1))
.Times(0);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// Register another buffer collection with that same ID.
sysmem_util::GlobalBufferCollectionId global_collection_id_2;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id_2](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id_2 = collection_id;
return true;
}));
{
flatland.RegisterBufferCollection(kBufferCollectionId, CreateToken());
// Skip session updates to test that release fences are what trigger the buffer importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_1))
.Times(0);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
}
// Signal the release fences, which should result deregister the first one from the importer.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_1))
.Times(1);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_2))
.Times(0);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
// Deregister the second one, signal the release fences, and verify the second global ID was
// deregistered.
flatland.DeregisterBufferCollection(kBufferCollectionId);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id_2))
.Times(1);
PRESENT(flatland, true);
}
// Tests that a buffer collection is not deregistered from the importer until it is not referenced
// by any active Image (including released Images still on Transforms) and the release fence is
// signaled.
TEST_F(FlatlandTest, DeregisterBufferCollectionWaitsForReleaseFence) {
Flatland flatland = CreateFlatland();
// Setup a valid buffer collection and Image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 2;
ImageProperties properties;
properties.set_width(100);
properties.set_height(200);
const auto global_id_pair =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
PRESENT(flatland, true);
// Deregister the buffer collection, but ensure that the deregistration call on the importer has
// not happened.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id))
.Times(0);
flatland.DeregisterBufferCollection(kBufferCollectionId);
PRESENT(flatland, true);
// 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_, ReleaseBufferCollection(global_collection_id))
.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_, ReleaseBufferCollection(global_collection_id))
.Times(0);
flatland.SetContentOnTransform(0, kTransformId);
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 deregistration call.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id))
.Times(1);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, DeregisterCollectionCompletesAfterFlatlandDestruction) {
sysmem_util::GlobalBufferCollectionId global_collection_id;
ContentId global_image_id;
{
Flatland flatland = CreateFlatland();
const ContentId kImageId = 3;
const BufferCollectionId kBufferCollectionId = 2;
ImageProperties properties;
properties.set_width(200);
properties.set_height(200);
auto global_id_pair =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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);
// Deregister the buffer collection.
flatland.DeregisterBufferCollection(kBufferCollectionId);
// Skip session updates to test that release fences are what trigger the importer calls.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id))
.Times(0);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseImage(global_image_id)).Times(0);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
// |flatland| falls out of scope.
}
// Reset the last known reference to the BufferImporter to demonstrate that the Wait keeps it
// alive.
buffer_collection_importer_.reset();
// Signal the release fences, which triggers the deregistration call, even though the Flatland
// instance and Renderer associated with the call have been cleaned up.
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id))
.Times(1);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseImage(global_image_id)).Times(1);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, ReleaseImageErrorCases) {
Flatland flatland = CreateFlatland();
// Zero is not a valid image ID.
flatland.ReleaseImage(0);
PRESENT(flatland, false);
// The image must exist.
flatland.ReleaseImage(1);
PRESENT(flatland, false);
// ContentId is not an Image.
ContentLinkToken parent_token;
GraphLinkToken child_token;
ASSERT_EQ(ZX_OK, zx::eventpair::create(0, &parent_token.value, &child_token.value));
const ContentId kLinkId = 2;
fidl::InterfacePtr<ContentLink> content_link;
LinkProperties properties;
properties.set_logical_size({kDefaultSize, kDefaultSize});
flatland.CreateLink(kLinkId, std::move(parent_token), std::move(properties),
content_link.NewRequest());
flatland.ReleaseImage(kLinkId);
PRESENT(flatland, false);
}
// If we have multiple BufferCollectionImporters, some of them may properly import
// a buffer collection while others do not. We have to therefore make sure that if
// improter A properly imports a buffer collection and then importer B fails, that
// Flatland automatically releases the buffer collection from importer A.
TEST_F(FlatlandTest, BufferCollectionImportPassesAndFailsOnDifferentImportersTest) {
// Create a second buffer collection importer.
auto local_mock_buffer_collection_importer = new MockBufferCollectionImporter();
auto local_buffer_collection_importer =
std::shared_ptr<BufferCollectionImporter>(local_mock_buffer_collection_importer);
// Create a flatland instance that has two BufferCollectionImporters.
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator;
auto session_id = scheduling::GetNextSessionId();
auto flatland = Flatland(session_id, flatland_presenter_, link_system_,
uber_struct_system_->AllocateQueueForSession(session_id),
{buffer_collection_importer_, local_buffer_collection_importer},
std::move(sysmem_allocator));
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(Return(true));
EXPECT_CALL(*local_mock_buffer_collection_importer, ImportBufferCollection(_, _, _))
.WillOnce(Return(false));
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(_)).Times(1);
EXPECT_CALL(*local_mock_buffer_collection_importer, ReleaseBufferCollection(_)).Times(0);
const auto kCollectionId = 2;
flatland.RegisterBufferCollection(kCollectionId, CreateToken());
}
// 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<BufferCollectionImporter>(local_mock_buffer_collection_importer);
// Create a flatland instance that has
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator;
auto session_id = scheduling::GetNextSessionId();
auto flatland = Flatland(session_id, flatland_presenter_, link_system_,
uber_struct_system_->AllocateQueueForSession(session_id),
{buffer_collection_importer_, local_buffer_collection_importer},
std::move(sysmem_allocator));
sysmem_util::GlobalBufferCollectionId global_collection_id;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(
testing::Invoke([&global_collection_id](
sysmem_util::GlobalBufferCollectionId collection_id,
fuchsia::sysmem::Allocator_Sync* sysmem_allocator,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
global_collection_id = collection_id;
return true;
}));
EXPECT_CALL(*local_mock_buffer_collection_importer, ImportBufferCollection(_, _, _))
.WillOnce(Return(true));
const auto kCollectionId = 2;
flatland.RegisterBufferCollection(kCollectionId, CreateToken());
ImageProperties properties;
properties.set_width(100);
properties.set_height(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 ReleaseImage().
EXPECT_CALL(*mock_buffer_collection_importer_, ImportImage(_)).WillOnce(Return(true));
EXPECT_CALL(*local_mock_buffer_collection_importer, ImportImage(_)).WillOnce(Return(false));
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseImage(_)).WillOnce(Return());
flatland.CreateImage(/*image_id*/ 1, kCollectionId, /*vmo_idx*/ 0, std::move(properties));
}
// Test to make sure that if a buffer collection importer returns |false|
// on |ImportImage()| that this is caught when we try to present.
TEST_F(FlatlandTest, BufferImporterImportImageReturnsFalseTest) {
Flatland flatland = CreateFlatland();
const auto kCollectionId = 3;
EXPECT_CALL(*mock_buffer_collection_importer_, ImportBufferCollection(_, _, _))
.WillOnce(Return(true));
flatland.RegisterBufferCollection(kCollectionId, CreateToken());
// Create a proper properties struct.
ImageProperties properties;
properties.set_width(150);
properties.set_height(175);
EXPECT_CALL(*mock_buffer_collection_importer_, ImportImage(_)).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, kCollectionId, /*vmo_idx*/ 0, std::move(properties));
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_, ImportImage(_)).WillOnce(Return(false));
// Import again, but this time have the importer return false. Flatland should catch
// this and PRESENT should return false.
properties.set_width(150);
properties.set_height(175);
flatland.CreateImage(/*image_id*/ 2, kCollectionId, /*vmo_idx*/ 0, std::move(properties));
PRESENT(flatland, false);
}
// Test to make sure that the release fences signal to the buffer importer
// to release the image.
TEST_F(FlatlandTest, BufferImporterImageReleaseTest) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
ImageProperties properties1;
properties1.set_width(100);
properties1.set_height(200);
const sysmem_util::GlobalBufferCollectionId global_collection_id1 =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
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.SetContentOnTransform(0, kTransformId);
PresentArgs args;
args.skip_session_update_and_release_fences = true;
PRESENT_WITH_ARGS(flatland, std::move(args), true);
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseImage(_)).Times(1);
ApplySessionUpdatesAndSignalFences();
RunLoopUntilIdle();
}
TEST_F(FlatlandTest, ReleasedImageRemainsUntilCleared) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
ImageProperties properties1;
properties1.set_width(100);
properties1.set_height(200);
const sysmem_util::GlobalBufferCollectionId global_collection_id =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
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);
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);
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.
flatland.SetContentOnTransform(0, kTransformId);
PRESENT(flatland, true);
uber_struct = GetUberStruct(flatland);
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) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
ImageProperties properties1;
properties1.set_width(100);
properties1.set_height(200);
const sysmem_util::GlobalBufferCollectionId global_collection_id1 =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId1);
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.
ImageProperties properties2;
properties2.set_width(300);
properties2.set_height(400);
const sysmem_util::GlobalBufferCollectionId global_collection_id2 =
CreateImage(&flatland, kImageId, 2, std::move(properties2)).collection_id;
const TransformId kTransformId2 = 3;
flatland.CreateTransform(kTransformId2);
flatland.AddChild(kTransformId1, kTransformId2);
flatland.SetContentOnTransform(kImageId, kTransformId2);
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);
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) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
ImageProperties properties1;
properties1.set_width(100);
properties1.set_height(200);
const sysmem_util::GlobalBufferCollectionId global_collection_id1 =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
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);
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);
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, ClearGraphReleasesImagesAndBufferCollections) {
Flatland flatland = CreateFlatland();
// Setup a valid image.
const ContentId kImageId = 1;
const BufferCollectionId kBufferCollectionId = 1;
ImageProperties properties1;
properties1.set_width(100);
properties1.set_height(200);
const sysmem_util::GlobalBufferCollectionId global_collection_id1 =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
PRESENT(flatland, true);
// Clear the graph, then signal the release fence and ensure the buffer collection is released.
flatland.ClearGraph();
EXPECT_CALL(*mock_buffer_collection_importer_, ReleaseBufferCollection(global_collection_id1))
.Times(1);
PRESENT(flatland, true);
// The buffer collection and Image ID should be available for re-use.
ImageProperties properties2;
properties2.set_width(400);
properties2.set_height(800);
const sysmem_util::GlobalBufferCollectionId global_collection_id2 =
CreateImage(&flatland, kImageId, kBufferCollectionId, 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.SetContentOnTransform(kImageId, kTransformId);
PRESENT(flatland, true);
}
#undef EXPECT_MATRIX
#undef PRESENT
} // namespace test
} // namespace flatland