blob: 34e56c159b46eef33bd37c93bdc2b9f7cbcc9d6f [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/scenic/lib/flatland/engine/engine.h"
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async-testing/test_loop.h>
#include <lib/async/cpp/wait.h>
#include <lib/async/default.h>
#include <lib/fdio/directory.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/eventpair.h>
#include <limits>
#include <thread>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/lib/fsl/handles/object_info.h"
#include "src/lib/testing/loop_fixture/real_loop_fixture.h"
#include "src/ui/scenic/lib/display/tests/mock_display_controller.h"
#include "src/ui/scenic/lib/flatland/engine/tests/mock_display_controller.h"
#include "src/ui/scenic/lib/flatland/flatland.h"
#include "src/ui/scenic/lib/flatland/global_image_data.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/renderer/null_renderer.h"
#include "src/ui/scenic/lib/flatland/renderer/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 flatland::ImageMetadata;
using flatland::LinkSystem;
using flatland::MockDisplayController;
using flatland::Renderer;
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::GraphLink;
using fuchsia::ui::scenic::internal::GraphLinkToken;
using fuchsia::ui::scenic::internal::LayoutInfo;
using fuchsia::ui::scenic::internal::LinkProperties;
namespace {
class EngineTest : public gtest::RealLoopFixture {
public:
EngineTest()
: uber_struct_system_(std::make_shared<UberStructSystem>()),
link_system_(std::make_shared<LinkSystem>(uber_struct_system_->GetNextInstanceId())) {}
void SetUp() override {
gtest::RealLoopFixture::SetUp();
// Create the SysmemAllocator.
zx_status_t status = fdio_service_connect(
"/svc/fuchsia.sysmem.Allocator", sysmem_allocator_.NewRequest().TakeChannel().release());
async_set_default_dispatcher(dispatcher());
renderer_ = std::make_shared<flatland::NullRenderer>();
zx::channel device_channel_server;
zx::channel device_channel_client;
FX_CHECK(ZX_OK == zx::channel::create(0, &device_channel_server, &device_channel_client));
zx::channel controller_channel_server;
zx::channel controller_channel_client;
FX_CHECK(ZX_OK ==
zx::channel::create(0, &controller_channel_server, &controller_channel_client));
mock_display_controller_ = std::make_unique<flatland::MockDisplayController>();
mock_display_controller_->Bind(std::move(device_channel_server),
std::move(controller_channel_server));
auto unique_display_controller =
std::make_unique<fuchsia::hardware::display::ControllerSyncPtr>();
unique_display_controller->Bind(std::move(controller_channel_client));
engine_ = std::make_unique<flatland::Engine>(std::move(unique_display_controller), renderer_,
link_system_, uber_struct_system_);
}
void TearDown() override {
sysmem_allocator_ = nullptr;
renderer_.reset();
engine_.reset();
mock_display_controller_.reset();
// Move the channel to a local variable which will go out of scope
// and close when this function returns.
auto local = local_.release();
gtest::RealLoopFixture::TearDown();
}
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;
}
class FakeFlatlandSession {
public:
FakeFlatlandSession(const std::shared_ptr<UberStructSystem>& uber_struct_system,
const std::shared_ptr<LinkSystem>& link_system, EngineTest* harness)
: uber_struct_system_(uber_struct_system),
link_system_(link_system),
harness_(harness),
id_(uber_struct_system_->GetNextInstanceId()),
graph_(id_),
queue_(uber_struct_system_->AllocateQueueForSession(id_)) {}
// Use the TransformGraph API to create and manage transforms and their children.
TransformGraph& graph() { return graph_; }
// Returns the link_origin for this session.
TransformHandle GetLinkOrigin() {
EXPECT_TRUE(parent_link_.has_value());
return parent_link_.value().parent_link.link_origin;
}
// Clears the ParentLink for this session, if one exists.
void ClearParentLink() { parent_link_.reset(); }
// Holds the ContentLink and LinkSystem::ChildLink objects since if they fall out of scope,
// the LinkSystem will delete the link. Tests should add |child_link.link_handle| to their
// TransformGraphs to use the ChildLink in a topology.
struct ChildLink {
fidl::InterfacePtr<ContentLink> content_link;
LinkSystem::ChildLink child_link;
// Returns the handle the parent should add as a child in its local topology to include the
// link in the topology.
TransformHandle GetLinkHandle() const { return child_link.link_handle; }
};
// Links this session to |parent_session| and returns the ChildLink, which should be used with
// the parent session. If the return value drops out of scope, tests should call
// ClearParentLink() on this session.
ChildLink LinkToParent(FakeFlatlandSession& parent_session) {
// Create the tokens.
ContentLinkToken parent_token;
GraphLinkToken child_token;
EXPECT_EQ(zx::eventpair::create(0, &parent_token.value, &child_token.value), ZX_OK);
// Create the parent link.
fidl::InterfacePtr<GraphLink> graph_link;
LinkSystem::ParentLink parent_link = link_system_->CreateParentLink(
std::move(child_token), graph_link.NewRequest(), graph_.CreateTransform());
// Create the child link.
fidl::InterfacePtr<ContentLink> content_link;
LinkSystem::ChildLink child_link = link_system_->CreateChildLink(
std::move(parent_token), LinkProperties(), content_link.NewRequest(),
parent_session.graph_.CreateTransform());
// Run the loop to establish the link.
harness_->RunLoopUntilIdle();
parent_link_ = ParentLink({
.graph_link = std::move(graph_link),
.parent_link = std::move(parent_link),
});
return ChildLink({
.content_link = std::move(content_link),
.child_link = std::move(child_link),
});
}
// Allocates a new UberStruct with a local_topology rooted at |local_root|. If this session has
// a ParentLink, the link_origin of that ParentLink will be used instead.
std::unique_ptr<UberStruct> CreateUberStructWithCurrentTopology(TransformHandle local_root) {
auto uber_struct = std::make_unique<UberStruct>();
// Only use the supplied |local_root| if no there is no ParentLink, otherwise use the
// |link_origin| from the ParentLink.
const TransformHandle root =
parent_link_.has_value() ? parent_link_.value().parent_link.link_origin : local_root;
// Compute the local topology and place it in the UberStruct.
auto local_topology_data =
graph_.ComputeAndCleanup(root, std::numeric_limits<uint64_t>::max());
EXPECT_NE(local_topology_data.iterations, std::numeric_limits<uint64_t>::max());
EXPECT_TRUE(local_topology_data.cyclical_edges.empty());
uber_struct->local_topology = local_topology_data.sorted_transforms;
return uber_struct;
}
// Pushes |uber_struct| to the UberStructSystem and updates the system so that it represents
// this session in the InstanceMap.
void PushUberStruct(std::unique_ptr<UberStruct> uber_struct) {
EXPECT_FALSE(uber_struct->local_topology.empty());
EXPECT_EQ(uber_struct->local_topology[0].handle.GetInstanceId(), id_);
queue_->Push(/*present_id=*/0, std::move(uber_struct));
uber_struct_system_->UpdateSessions({{id_, 0}});
}
private:
// Shared systems for all sessions.
std::shared_ptr<UberStructSystem> uber_struct_system_;
std::shared_ptr<LinkSystem> link_system_;
// The test harness to give access to RunLoopUntilIdle().
EngineTest* harness_;
// Data specific this session.
scheduling::SessionId id_;
TransformGraph graph_;
std::shared_ptr<UberStructSystem::UberStructQueue> queue_;
// Holds the GraphLink and LinkSystem::ParentLink objects since if they fall out of scope,
// the LinkSystem will delete the link. When |parent_link_| has a value, the
// |parent_link.link_origin| from this object is used as the root TransformHandle.
struct ParentLink {
fidl::InterfacePtr<GraphLink> graph_link;
LinkSystem::ParentLink parent_link;
};
std::optional<ParentLink> parent_link_;
};
FakeFlatlandSession CreateSession() {
return FakeFlatlandSession(uber_struct_system_, link_system_, this);
}
protected:
// Systems that are populated with data from Flatland instances.
const std::shared_ptr<UberStructSystem> uber_struct_system_;
const std::shared_ptr<LinkSystem> link_system_;
std::shared_ptr<flatland::NullRenderer> renderer_;
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator_;
std::unique_ptr<flatland::Engine> engine_;
std::unique_ptr<flatland::MockDisplayController> mock_display_controller_;
private:
zx::channel local_;
};
} // namespace
namespace flatland {
namespace test {
TEST_F(EngineTest, ImportAndReleaseBufferCollectionTest) {
auto mock = mock_display_controller_.get();
// Set the mock display controller functions and wait for messages.
std::thread server([&mock]() mutable {
// Wait once for call to ImportBufferCollection, once for setting the
// constraints, and once for call to ReleaseBufferCollection
for (uint32_t i = 0; i < 3; i++) {
mock->WaitForMessage();
}
});
const sysmem_util::GlobalBufferCollectionId kGlobalBufferCollectionId = 15;
EXPECT_CALL(*mock_display_controller_.get(),
ImportBufferCollection(kGlobalBufferCollectionId, _, _))
.WillOnce(testing::Invoke(
[](uint64_t, fidl::InterfaceHandle<class ::fuchsia::sysmem::BufferCollectionToken>,
MockDisplayController::ImportBufferCollectionCallback callback) { callback(ZX_OK); }));
EXPECT_CALL(*mock_display_controller_.get(),
SetBufferCollectionConstraints(kGlobalBufferCollectionId, _, _))
.WillOnce(testing::Invoke(
[](uint64_t collection_id, fuchsia::hardware::display::ImageConfig config,
MockDisplayController::SetBufferCollectionConstraintsCallback callback) {
callback(ZX_OK);
}));
engine_->ImportBufferCollection(kGlobalBufferCollectionId, nullptr, CreateToken());
EXPECT_CALL(*mock_display_controller_, ReleaseBufferCollection(kGlobalBufferCollectionId))
.WillOnce(Return());
engine_->ReleaseBufferCollection(kGlobalBufferCollectionId);
server.join();
}
TEST_F(EngineTest, ImportImageErrorCases) {
const sysmem_util::GlobalBufferCollectionId kGlobalBufferCollectionId = 30;
const flatland::GlobalImageId kImageId = 50;
const uint32_t kVmoCount = 2;
const uint32_t kVmoIdx = 1;
const uint32_t kMaxWidth = 100;
const uint32_t kMaxHeight = 200;
uint32_t num_times_import_image_called = 0;
EXPECT_CALL(*mock_display_controller_.get(),
ImportBufferCollection(kGlobalBufferCollectionId, _, _))
.WillOnce(testing::Invoke(
[](uint64_t, fidl::InterfaceHandle<class ::fuchsia::sysmem::BufferCollectionToken>,
MockDisplayController::ImportBufferCollectionCallback callback) { callback(ZX_OK); }));
EXPECT_CALL(*mock_display_controller_.get(),
SetBufferCollectionConstraints(kGlobalBufferCollectionId, _, _))
.WillOnce(testing::Invoke(
[](uint64_t collection_id, fuchsia::hardware::display::ImageConfig config,
MockDisplayController::SetBufferCollectionConstraintsCallback callback) {
callback(ZX_OK);
}));
// Set the mock display controller functions and wait for messages.
auto mock = mock_display_controller_.get();
std::thread server([&mock]() mutable {
// Wait once for call to ImportBufferCollection, once for setting
// the buffer collection constraints, a single valid call to
// ImportImage() 1 invalid call to ImportImage(), and a single
// call to ReleaseImage(). Although there are more than three
// invalid calls to ImportImage() below, only 3 of them make it
// all the way to the display controller, which is why we only
// have to wait 3 times.
for (uint32_t i = 0; i < 5; i++) {
mock->WaitForMessage();
}
});
engine_->ImportBufferCollection(kGlobalBufferCollectionId, nullptr, CreateToken());
ImageMetadata metadata = {
.collection_id = kGlobalBufferCollectionId,
.identifier = kImageId,
.vmo_idx = kVmoIdx,
.width = 20,
.height = 30,
};
// Make sure that the engine returns true if the display controller returns true.
const uint64_t kDisplayImageId = 70;
EXPECT_CALL(*mock_display_controller_.get(),
ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _))
.WillRepeatedly(testing::Invoke([](fuchsia::hardware::display::ImageConfig image_config,
uint64_t collection_id, uint32_t index,
MockDisplayController::ImportImageCallback callback) {
callback(ZX_OK, /*display_image_id*/kDisplayImageId);
}));
auto result = engine_->ImportImage(metadata);
EXPECT_TRUE(result);
// Make sure we can release the image properly.
EXPECT_CALL(*mock_display_controller_, ReleaseImage(kDisplayImageId)).WillOnce(Return());
engine_->ReleaseImage(metadata.identifier);
// Make sure that the engine returns false if the display controller returns an error
EXPECT_CALL(*mock_display_controller_.get(),
ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _))
.WillRepeatedly(testing::Invoke([](fuchsia::hardware::display::ImageConfig image_config,
uint64_t collection_id, uint32_t index,
MockDisplayController::ImportImageCallback callback) {
callback(ZX_ERR_INVALID_ARGS, /*display_image_id*/ 0);
}));
result = engine_->ImportImage(metadata);
EXPECT_FALSE(result);
// Collection ID can't be invalid. This shouldn't reach the display controller.
EXPECT_CALL(*mock_display_controller_.get(),
ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _))
.Times(0);
auto copy_metadata = metadata;
copy_metadata.collection_id = sysmem_util::kInvalidId;
result = engine_->ImportImage(copy_metadata);
EXPECT_FALSE(result);
// Image Id can't be 0. This shouldn't reach the display controller.
EXPECT_CALL(*mock_display_controller_.get(),
ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _))
.Times(0);
copy_metadata = metadata;
copy_metadata.identifier = 0;
result = engine_->ImportImage(copy_metadata);
EXPECT_FALSE(result);
// Width can't be 0. This shouldn't reach the display controller.
EXPECT_CALL(*mock_display_controller_.get(),
ImportImage(_, kGlobalBufferCollectionId, kVmoIdx, _))
.Times(0);
copy_metadata = metadata;
copy_metadata.width = 0;
result = engine_->ImportImage(copy_metadata);
EXPECT_FALSE(result);
// Height can't be 0. This shouldn't reach the display controller.
EXPECT_CALL(*mock_display_controller_.get(), ImportImage(_, _, 0, _)).Times(0);
copy_metadata = metadata;
copy_metadata.height = 0;
result = engine_->ImportImage(copy_metadata);
EXPECT_FALSE(result);
server.join();
}
// When compositing directly to a hardware display layer, the display controller
// takes in source and destination Frame object types, which mirrors flatland usage.
// The source frames are nonnormalized UV coordinates and the destination frames are
// screenspace coordinates given in pixels. So this test makes sure that the rectangle
// and frame data that is generated by flatland sends along to the display controller
// the proper source and destination frame data. Each source and destination frame pair
// should be added to its own layer on the display.
TEST_F(EngineTest, HardwareFrameCorrectnessTest) {
// Create a parent and child session.
auto parent_session = CreateSession();
auto child_session = CreateSession();
// Create a link between the two.
auto child_link = child_session.LinkToParent(parent_session);
// Create the root handle for the parent and a handle that will have an image attached.
const TransformHandle parent_root_handle = parent_session.graph().CreateTransform();
const TransformHandle parent_image_handle = parent_session.graph().CreateTransform();
// Add the two children to the parent root: link, then image.
parent_session.graph().AddChild(parent_root_handle, child_link.GetLinkHandle());
parent_session.graph().AddChild(parent_root_handle, parent_image_handle);
// Create an image handle for the child.
const TransformHandle child_image_handle = child_session.graph().CreateTransform();
// Attach that image handle to the link_origin.
child_session.graph().AddChild(child_session.GetLinkOrigin(), child_image_handle);
// Get an UberStruct for the parent session.
auto parent_struct = parent_session.CreateUberStructWithCurrentTopology(parent_root_handle);
// Add an image.
parent_struct->images[parent_image_handle] = ImageMetadata({
.vmo_idx = 1,
.width = 128,
.height = 256,
});
parent_struct->local_matrices[parent_image_handle] = glm::mat3(1);
parent_struct->local_matrices[parent_image_handle] =
glm::scale(glm::translate(glm::mat3(1.0), glm::vec2(9, 13)), glm::vec2(10, 20));
// Submit the UberStruct.
parent_session.PushUberStruct(std::move(parent_struct));
// Get an UberStruct for the child session. Note that the argument will be ignored anyway.
auto child_struct =
child_session.CreateUberStructWithCurrentTopology(child_session.GetLinkOrigin());
// Add an image.
child_struct->images[child_image_handle] = ImageMetadata({
.vmo_idx = 2,
.width = 512,
.height = 1024,
});
child_struct->local_matrices[child_image_handle] =
glm::scale(glm::translate(glm::mat3(1), glm::vec2(5, 7)), glm::vec2(30, 40));
// Submit the UberStruct.
child_session.PushUberStruct(std::move(child_struct));
uint64_t display_id = 1;
glm::uvec2 resolution(1024, 768);
// We will end up with 2 source frames, 2 destination frames, and two layers beind sent to the
// display.
fuchsia::hardware::display::Frame sources[2] = {
{.x_pos = 0u, .y_pos = 0u, .width = 512, .height = 1024u},
{.x_pos = 0u, .y_pos = 0u, .width = 128u, .height = 256u}};
fuchsia::hardware::display::Frame destinations[2] = {
{.x_pos = 5u, .y_pos = 7u, .width = 30, .height = 40u},
{.x_pos = 9u, .y_pos = 13u, .width = 10u, .height = 20u}};
// Setup the EXPECT_CALLs for gmock.
uint64_t layer_id = 1;
EXPECT_CALL(*mock_display_controller_.get(), CreateLayer(_))
.WillRepeatedly(testing::Invoke([&](MockDisplayController::CreateLayerCallback callback) {
callback(ZX_OK, layer_id++);
}));
std::vector<uint64_t> layers = {1u, 2u};
EXPECT_CALL(*mock_display_controller_.get(), SetDisplayLayers(display_id, layers)).Times(1);
// Unfortunately, |fuchsia::hardware::display::Frame| doesn't have an equality operator, so
// we can't just pass in the values we're expecting into the function as parameters. We can
// still use fidl::Equals inside the function body, however.
EXPECT_CALL(*mock_display_controller_.get(), SetLayerPrimaryPosition(layers[0], _, _, _))
.WillOnce(
testing::Invoke([&](uint64_t layer_id, fuchsia::hardware::display::Transform transform,
fuchsia::hardware::display::Frame src_frame,
fuchsia::hardware::display::Frame dest_frame) {
EXPECT_TRUE(fidl::Equals(src_frame, sources[0]));
EXPECT_TRUE(fidl::Equals(dest_frame, destinations[0]));
}));
EXPECT_CALL(*mock_display_controller_.get(), SetLayerPrimaryPosition(layers[1], _, _, _))
.WillOnce(
testing::Invoke([&](uint64_t layer_id, fuchsia::hardware::display::Transform transform,
fuchsia::hardware::display::Frame src_frame,
fuchsia::hardware::display::Frame dest_frame) {
EXPECT_TRUE(fidl::Equals(src_frame, sources[1]));
EXPECT_TRUE(fidl::Equals(dest_frame, destinations[1]));
}));
// Set the mock display controller functions and wait for messages.
auto mock = mock_display_controller_.get();
std::thread server([&mock]() mutable {
// Since we have 2 rectangles with images, we have to wait for 2 calls to initialize layers,
// 1 call to set the layers on the display, and 2 calls to set the layer primary positions.
// This all happens when we call engine_->RenderFrame() below.
for (uint32_t i = 0; i < 5; i++) {
mock->WaitForMessage();
}
});
engine_->AddDisplay(display_id, parent_root_handle, resolution);
engine_->RenderFrame();
server.join();
}
} // namespace test
} // namespace flatland