blob: d25fb535b5c95a18f17f065a392db7bcfc942e04 [file] [log] [blame]
// Copyright 2018 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.
// clang-format off
#include "src/ui/lib/glm_workaround/glm_workaround.h"
// clang-format on
#include <fuchsia/images/cpp/fidl.h>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <gtest/gtest.h>
#include <lib/fdio/directory.h>
#include <lib/ui/scenic/cpp/commands.h>
#include <lib/ui/scenic/cpp/session.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/images/cpp/images.h>
#include <lib/zx/clock.h>
#include <zircon/types.h>
#include <map>
#include <string>
#include "src/lib/fsl/handles/object_info.h"
#include <lib/syslog/cpp/macros.h>
#include "src/ui/scenic/lib/gfx/tests/pixel_test.h"
#include "src/ui/scenic/lib/gfx/tests/vk_session_test.h"
#include "src/ui/scenic/lib/gfx/tests/vk_util.h"
#include "src/ui/testing/views/background_view.h"
#include "src/ui/testing/views/coordinate_test_view.h"
#include "src/ui/testing/views/opacity_view.h"
#include "src/ui/testing/views/test_view.h"
#include "src/ui/lib/escher/impl/vulkan_utils.h"
#include "src/ui/lib/escher/hmd/pose_buffer.h"
#include "src/ui/lib/escher/impl/naive_image.h"
#include "src/ui/lib/escher/renderer/batch_gpu_uploader.h"
#include "src/ui/lib/escher/test/common/gtest_escher.h"
#include "src/ui/lib/escher/test/common/gtest_vulkan.h"
#include "src/ui/lib/escher/util/fuchsia_utils.h"
#include "src/ui/lib/escher/util/image_utils.h"
#include "src/ui/lib/yuv/yuv.h"
#include <vulkan/vulkan.hpp>
namespace {
constexpr char kEnvironment[] = "ScenicPixelTest";
// If you change the size of YUV buffers, make sure that the YUV test in
// host_image_unittest.cc is also updated. Unlike that unit test,
// scenic_pixel_test.cc has no way to confirm that it is going through the
// direct-to-GPU path.
// TODO(SCN-1387): This number needs to be queried via sysmem or vulkan.
constexpr uint32_t kYuvSize = 64;
const float kPi = glm::pi<float>();
class ScenicPixelTest : public gfx::PixelTest {
protected:
ScenicPixelTest() : gfx::PixelTest(kEnvironment) {}
};
TEST_F(ScenicPixelTest, SolidColor) {
scenic::BackgroundView view(CreatePresentationContext());
view.SetBackgroundColor(scenic::BackgroundView::kBackgroundColor);
RunUntilIndirectPresent(&view);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
// We could assert on each pixel individually, but a histogram might give us a
// more meaningful failure.
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[scenic::BackgroundView::kBackgroundColor], 0u);
histogram.erase(scenic::BackgroundView::kBackgroundColor);
// This assert is written this way so that, when it fails, it prints out all
// the unexpected colors
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
TEST_F(ScenicPixelTest, PresentOrReplaceView_ShouldReplacePreviousPresentation) {
scenic::BackgroundView view(CreatePresentationContext());
view.SetBackgroundColor(scenic::BackgroundView::kBackgroundColor);
RunUntilIndirectPresent(&view);
{
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[scenic::BackgroundView::kBackgroundColor], 0u);
histogram.erase(scenic::BackgroundView::kBackgroundColor);
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
const scenic::Color kNewBackgroundColor = {0xFF, 0x00, 0xFF, 0xFF};
ASSERT_FALSE(kNewBackgroundColor == scenic::BackgroundView::kBackgroundColor);
{
// Clobber current presentation with a new one with different background. Check that the
// background changes.
scenic::BackgroundView view2(CreatePresentationContext(/*clobber=*/true));
view2.SetBackgroundColor(kNewBackgroundColor);
RunUntilIndirectPresent(&view2);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[kNewBackgroundColor], 0u);
histogram.erase(kNewBackgroundColor);
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
}
TEST_F(ScenicPixelTest, NV12Texture) {
scenic::BackgroundView view(CreatePresentationContext());
fuchsia::images::ImageInfo image_info{
.width = kYuvSize,
.height = kYuvSize,
.stride = static_cast<uint32_t>(
kYuvSize * images::StrideBytesPerWidthPixel(fuchsia::images::PixelFormat::NV12)),
.pixel_format = fuchsia::images::PixelFormat::NV12,
};
uint32_t num_pixels = image_info.width * image_info.height;
uint64_t image_vmo_bytes = images::ImageSize(image_info);
EXPECT_EQ((3 * num_pixels) / 2, image_vmo_bytes);
zx::vmo image_vmo;
zx_status_t status = zx::vmo::create(image_vmo_bytes, 0, &image_vmo);
EXPECT_EQ(ZX_OK, status);
uint8_t* vmo_base;
status = zx::vmar::root_self()->map(0, image_vmo, 0, image_vmo_bytes,
ZX_VM_PERM_WRITE | ZX_VM_PERM_READ,
reinterpret_cast<uintptr_t*>(&vmo_base));
EXPECT_EQ(ZX_OK, status);
static const uint8_t kYValue = 110;
static const uint8_t kUValue = 192;
static const uint8_t kVValue = 192;
const scenic::Color kBgraColor = {0xF1, 0x87, 0xFA, 0xFF};
// Set all the Y pixels at full res.
for (uint32_t i = 0; i < num_pixels; ++i) {
vmo_base[i] = kYValue;
}
// Set all the UV pixels pairwise at half res.
for (uint32_t i = num_pixels; i < num_pixels + num_pixels / 2; i += 2) {
vmo_base[i] = kUValue;
vmo_base[i + 1] = kVValue;
}
view.SetImage(std::move(image_vmo), image_vmo_bytes, image_info,
fuchsia::images::MemoryType::HOST_MEMORY);
RunUntilIndirectPresent(&view);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
// We could assert on each pixel individually, but a histogram might give us a
// more meaningful failure.
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[kBgraColor], 0u);
histogram.erase(kBgraColor);
// This assert is written this way so that, when it fails, it prints out all
// the unexpected colors
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
TEST_F(ScenicPixelTest, ViewCoordinates) {
scenic::CoordinateTestView view(CreatePresentationContext());
RunUntilIndirectPresent(&view);
scenic::Screenshot screenshot = TakeScreenshot();
EXPECT_EQ(scenic::CoordinateTestView::kUpperLeft, screenshot.ColorAt(.25f, .25f));
EXPECT_EQ(scenic::CoordinateTestView::kUpperRight, screenshot.ColorAt(.25f, .75f));
EXPECT_EQ(scenic::CoordinateTestView::kLowerLeft, screenshot.ColorAt(.75f, .25f));
EXPECT_EQ(scenic::CoordinateTestView::kLowerRight, screenshot.ColorAt(.75f, .75f));
EXPECT_EQ(scenic::CoordinateTestView::kCenter, screenshot.ColorAt(.5f, .5f));
}
// Draws and tests the following coordinate test pattern without views:
// ___________________________________
// | | |
// | BLACK | RED |
// | _____|_____ |
// |___________| GREEN |___________|
// | |_________| |
// | | |
// | BLUE | MAGENTA |
// |________________|________________|
//
TEST_F(ScenicPixelTest, GlobalCoordinates) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
const float pane_width = display_width / 2;
const float pane_height = display_height / 2;
for (uint32_t i = 0; i < 2; i++) {
for (uint32_t j = 0; j < 2; j++) {
scenic::Rectangle pane_shape(session, pane_width, pane_height);
scenic::Material pane_material(session);
pane_material.SetColor(i * 255.f, 0, j * 255.f, 255);
scenic::ShapeNode pane_node(session);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation((i + .5f) * pane_width, (j + .5f) * pane_height, -20);
scene->AddChild(pane_node);
}
}
scenic::Rectangle pane_shape(session, display_width / 4, display_height / 4);
scenic::Material pane_material(session);
pane_material.SetColor(0, 255, 0, 255);
scenic::ShapeNode pane_node(session);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation(.5f * display_width, .5f * display_height, -40);
scene->AddChild(pane_node);
// Actual tests. Test the same scene with an orthographic and perspective
// camera.
std::string camera_type[2] = {"orthographic", "perspective"};
auto camera = test_session->SetUpCamera();
float fov[2] = {0, 2 * atan((display_height / 2.f) / gfx::TestSession::kDefaultCameraOffset)};
for (int i = 0; i < 2; i++) {
FX_LOGS(INFO) << "Testing " << camera_type[i] << " camera";
camera.SetProjection(fov[i]);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
EXPECT_EQ(scenic::Color({0, 0, 0, 255}), screenshot.ColorAt(.25f, .25f));
EXPECT_EQ(scenic::Color({0, 0, 255, 255}), screenshot.ColorAt(.25f, .75f));
EXPECT_EQ(scenic::Color({255, 0, 0, 255}), screenshot.ColorAt(.75f, .25f));
EXPECT_EQ(scenic::Color({255, 0, 255, 255}), screenshot.ColorAt(.75f, .75f));
EXPECT_EQ(scenic::Color({0, 255, 0, 255}), screenshot.ColorAt(.5f, .5f));
}
}
// Draws a white rectangle on a black background rendered with a stereo
// camera, which produces an image something like this:
// _____________________________________
// | |
// | ___________ ___________ |
// | | | | | |
// | | | | | |
// | | WHITE | BLACK | WHITE | |
// | | | | | |
// | |_________| |_________| |
// | |
// |___________________________________|
//
TEST_F(ScenicPixelTest, StereoCamera) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
const float viewport_width = display_width / 2;
const float viewport_height = display_height;
float fovy = 2 * atan((display_height / 2.f) / gfx::TestSession::kDefaultCameraOffset);
glm::mat4 projection = glm::perspective(fovy, viewport_width / viewport_height, 0.1f,
gfx::TestSession::kDefaultCameraOffset);
projection = glm::scale(projection, glm::vec3(1.f, -1.f, 1.f));
std::array<float, 16> projection_arr;
auto projection_ptr = glm::value_ptr(projection);
std::copy(projection_ptr, projection_ptr + 16, std::begin(projection_arr));
test_session->SetUpCamera<scenic::StereoCamera>().SetStereoProjection(projection_arr,
projection_arr);
const float pane_width = viewport_width / 2;
const float pane_height = viewport_height / 2;
glm::vec3 translation(.5f * display_width, .5f * display_height, -10);
scenic::Rectangle pane_shape(session, pane_width, pane_height);
scenic::Material pane_material(session);
pane_material.SetColor(255, 255, 255, 255);
scenic::ShapeNode pane_shape_node(session);
pane_shape_node.SetShape(pane_shape);
pane_shape_node.SetMaterial(pane_material);
pane_shape_node.SetTranslation(translation.x, translation.y, translation.z);
test_session->scene.AddChild(pane_shape_node);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
// Color array to index 0=BLACK 1=WHITE
scenic::Color colors[2] = {scenic::Color{0, 0, 0, 0}, scenic::Color{255, 255, 255, 255}};
// Expected results by index into colors array. Column major.
// Note how this is a transposed, low-res version of the scene being drawn.
// clang-format off
int expected[8][4] = {{0, 0, 0, 0},
{0, 1, 1, 0},
{0, 1, 1, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
{0, 1, 1, 0},
{0, 1, 1, 0},
{0, 0, 0, 0}};
// clang-format on
// Test 8 columns of 4 samples each
int num_x_samples = 8;
int num_y_samples = 4;
float x_step = 1.f / num_x_samples;
float y_step = 1.f / num_y_samples;
// i maps to x, j maps to y
for (int i = 0; i < num_x_samples; i++) {
for (int j = 0; j < num_y_samples; j++) {
float x = x_step / 2 + i * x_step;
float y = y_step / 2 + j * y_step;
EXPECT_EQ(colors[expected[i][j]], screenshot.ColorAt(x, y))
<< "i = " << i << ", j = " << j << ", Sample Location: {" << x << ", " << y << "}";
}
}
}
// At a high level this test puts a camera inside a cube where each face is a
// different color, then uses a pose buffer to point the camera at different
// faces, using the colors to verify the pose buffer is working as expected.
TEST_F(ScenicPixelTest, PoseBuffer) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
const float viewport_width = display_width / 2;
const float viewport_height = display_height;
static constexpr float kCameraOffset = 500;
// View matrix matches vulkan clip space +Y down, looking in direction of +Z
const glm::vec3 eye(display_width / 2.f, display_height / 2.f, -kCameraOffset);
const glm::vec3 look_at(eye + glm::vec3(0, 0, 1));
const std::array<float, 3> up = {0, -1, 0};
scenic::StereoCamera camera(test_session->scene);
camera.SetTransform({eye.x, eye.y, eye.z}, {look_at.x, look_at.y, look_at.z}, up);
glm::mat4 projection =
glm::perspective(glm::radians(120.f), viewport_width / viewport_height, 0.1f, kCameraOffset);
// projection = glm::scale(projection, glm::vec3(1.f, -1.f, 1.f));
// clang-format off
glm::mat4 clip(1.0f, 0.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.5f, 0.0f,
0.0f, 0.0f, 0.5f, 1.0f);
// clang-format on
projection = clip * projection;
std::array<float, 16> projection_arr;
auto projection_ptr = glm::value_ptr(projection);
std::copy(projection_ptr, projection_ptr + 16, std::begin(projection_arr));
camera.SetStereoProjection(projection_arr, projection_arr);
test_session->renderer.SetCamera(camera.id());
// Configure PoseBuffer
const size_t kVmoSize = PAGE_SIZE;
zx_status_t status;
auto vulkan_queues = scenic_impl::gfx::test::VkSessionTest::CreateVulkanDeviceQueues();
auto device = vulkan_queues->vk_device();
auto physical_device = vulkan_queues->vk_physical_device();
// TODO(SCN-1369): Scenic may use a different set of bits when creating a
// buffer, resulting in a memory pool mismatch.
const vk::BufferUsageFlags kUsageFlags =
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst |
vk::BufferUsageFlagBits::eStorageTexelBuffer | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eVertexBuffer;
auto memory_requirements =
scenic_impl::gfx::test::GetBufferRequirements(device, kVmoSize, kUsageFlags);
auto memory = scenic_impl::gfx::test::AllocateExportableMemory(
device, physical_device, memory_requirements,
vk::MemoryPropertyFlagBits::eDeviceLocal | vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCached);
// If we can't make memory that is both host-visible and device-local, we
// can't run this test.
if (!memory) {
FX_LOGS(INFO) << "Could not find UMA compatible memory pool, aborting test.";
return;
}
zx::vmo pose_buffer_vmo =
scenic_impl::gfx::test::ExportMemoryAsVmo(device, vulkan_queues->dispatch_loader(), memory);
zx::vmo remote_vmo;
status = pose_buffer_vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &remote_vmo);
FX_CHECK(status == ZX_OK);
zx::time base_time = zx::clock::get_monotonic();
// Normally the time interval is the period of time between each entry in the
// pose buffer. In this example we only use one entry so the time interval is
// pretty meaningless. Set to 1 for simplicity (see ARGO-21).
zx::duration time_interval(1);
uint32_t num_entries = 1;
scenic::Memory mem(session, std::move(remote_vmo), kVmoSize,
fuchsia::images::MemoryType::VK_DEVICE_MEMORY);
scenic::Buffer pose_buffer(mem, 0, kVmoSize);
camera.SetPoseBuffer(pose_buffer, num_entries, base_time, time_interval);
// Set up scene.
static constexpr float kPaneWidth = kCameraOffset / 2.f;
scenic::Rectangle pane_shape(session, kPaneWidth, kPaneWidth);
static const int num_panes = 6;
scenic::Color colors[num_panes] = {
scenic::Color({255, 0, 0, 255}), // RED
scenic::Color({0, 255, 255, 255}), // CYAN
scenic::Color({0, 255, 0, 255}), // GREEN
scenic::Color({255, 0, 255, 255}), // MAGENTA
scenic::Color({0, 0, 255, 255}), // BLUE
scenic::Color({255, 255, 0, 255}), // YELLOW
};
static constexpr float kPaneOffset = kPaneWidth / 2;
glm::vec3 translations[num_panes] = {
eye + glm::vec3(0, 0, kPaneOffset), // In front of camera.
eye + glm::vec3(0, 0, -kPaneOffset), // Behind camera.
eye + glm::vec3(-kPaneOffset, 0, 0), // Left of Camera
eye + glm::vec3(kPaneOffset, 0, 0), // Right of camera
eye + glm::vec3(0, -kPaneOffset, 0), // Above Camera
eye + glm::vec3(0, kPaneOffset, 0), // Below Camera
};
glm::quat orientations[num_panes] = {
glm::quat(), // identity quaternion
glm::angleAxis(kPi, glm::vec3(1, 0, 0)),
glm::angleAxis(-kPi / 2, glm::vec3(0, 1, 0)),
glm::angleAxis(kPi / 2, glm::vec3(0, 1, 0)),
glm::angleAxis(kPi / 2, glm::vec3(1, 0, 0)),
glm::angleAxis(-kPi / 2, glm::vec3(1, 0, 0)),
};
for (int i = 0; i < num_panes; i++) {
scenic::Color color = colors[i];
glm::vec3 translation = translations[i];
glm::quat orientation = orientations[i];
FX_LOGS(ERROR) << "translation: " << glm::to_string(translation);
FX_LOGS(ERROR) << "orientation: " << glm::to_string(orientation);
scenic::Material pane_material(session);
pane_material.SetColor(color.r, color.g, color.b, color.a);
scenic::ShapeNode pane_shape_node(session);
pane_shape_node.SetShape(pane_shape);
pane_shape_node.SetMaterial(pane_material);
pane_shape_node.SetTranslation(translation.x, translation.y, translation.z);
pane_shape_node.SetRotation(orientation.x, orientation.y, orientation.z, orientation.w);
scene->AddChild(pane_shape_node);
}
static const int num_quaternions = 8;
glm::quat quaternions[num_quaternions] = {
glm::quat(), // dead ahead
glm::angleAxis(kPi, glm::vec3(0, 0, 1)), // dead ahead but upside down
glm::angleAxis(kPi, glm::vec3(1, 0, 0)), // behind around X
glm::angleAxis(kPi, glm::vec3(0, 1, 0)), // behind around Y
glm::angleAxis(kPi / 2, glm::vec3(0, 1, 0)), // left
glm::angleAxis(-kPi / 2, glm::vec3(0, 1, 0)), // right
glm::angleAxis(kPi / 2, glm::vec3(1, 0, 0)), // up
glm::angleAxis(-kPi / 2, glm::vec3(1, 0, 0)), // down
};
int expected_color_index[num_quaternions] = {0, 0, 1, 1, 2, 3, 4, 5};
uintptr_t ptr;
status = zx::vmar::root_self()->map(0, pose_buffer_vmo, 0, kVmoSize,
ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, &ptr);
FX_CHECK(status == ZX_OK);
auto pose_buffer_ptr = reinterpret_cast<escher::hmd::Pose*>(ptr);
for (int i = 0; i < num_quaternions; i++) {
// Put pose into pose buffer.
// Only testing orientation so position is always the origin.
// Quaternion describes head orientation, so invert it to get a transform
// that takes you into head space.
escher::hmd::Pose pose(glm::inverse(quaternions[i]), glm::vec3(0, 0, 0));
*pose_buffer_ptr = pose;
// Manually flush the buffer so this works on ARM
status = pose_buffer_vmo.op_range(ZX_VMO_OP_CACHE_CLEAN, 0, kVmoSize, nullptr, 0);
FX_CHECK(status == ZX_OK);
Present(session);
EXPECT_EQ(colors[expected_color_index[i]], TakeScreenshot().ColorAt(0.25, 0.5)) << "i = " << i;
}
device.freeMemory(memory);
}
struct OpacityTestParams {
float opacity;
scenic::Color expected_color;
};
class ParameterizedOpacityPixelTest : public ScenicPixelTest,
public ::testing::WithParamInterface<OpacityTestParams> {};
TEST_P(ParameterizedOpacityPixelTest, CheckPixels) {
constexpr auto COMPARE_COLOR = [](const scenic::Color& color_1, const scenic::Color& color_2,
int max_error) {
EXPECT_TRUE(abs(color_1.r - color_2.r) <= max_error &&
abs(color_1.g - color_2.g) <= max_error &&
abs(color_1.b - color_2.b) <= max_error && abs(color_1.a - color_2.a) <= max_error)
<< "Color " << color_1 << " and " << color_2 << " don't match.";
};
OpacityTestParams test_params = GetParam();
scenic::OpacityView view(CreatePresentationContext());
view.set_background_color(0xff, 0x00, 0xf0);
view.set_foreground_color(0x00, 0xff, 0x0f);
view.set_foreground_opacity(test_params.opacity);
RunUntilIndirectPresent(&view);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
// We could assert on each pixel individually, but a histogram might give us
// a more meaningful failure.
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
// There should be only one color here in the histogram.
COMPARE_COLOR(histogram.begin()->first, test_params.expected_color, 1);
}
// We use the same background/foreground color for each test iteration, but
// vary the opacity. When the opacity is 0% we expect the pure background
// color, and when it is 100% we expect the pure foreground color. When
// opacity is 50% we expect a blend of the two.
INSTANTIATE_TEST_SUITE_P(
Opacity, ParameterizedOpacityPixelTest,
::testing::Values(
OpacityTestParams{.opacity = 0.0f, .expected_color = {0xff, 0x00, 0xf0, 0xff}},
OpacityTestParams{.opacity = 0.5f, .expected_color = {0xbb, 0xbb, 0xb1, 0xff}},
OpacityTestParams{.opacity = 1.0f, .expected_color = {0x00, 0xff, 0x0f, 0xff}}));
TEST_F(ScenicPixelTest, ViewBoundClipping) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
test_session->SetUpCamera().SetProjection(0);
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
scenic::View view(session, std::move(view_token), "ClipView");
scenic::ViewHolder view_holder(session, std::move(view_holder_token), "ClipViewHolder");
static const std::array<float, 3> bmin = {0.f, 0.f, -2.f};
const std::array<float, 3> bmax = {display_width / 2, display_height, 1.f};
static const std::array<float, 3> imin = {0, 0, 0};
static const std::array<float, 3> imax = {0, 0, 0};
view_holder.SetViewProperties(bmin, bmax, imin, imax);
// Pane extends all the way across the screen horizontally, but
// the view is only on the left-hand side of the screen.
int32_t pane_width = display_width;
int32_t pane_height = 0.25 * display_height;
scenic::Rectangle pane_shape(session, pane_width, pane_height);
scenic::Material pane_material(session);
pane_material.SetColor(255, 0, 255, 255); // Magenta.
scenic::ShapeNode pane_node(session);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation(0.5 * pane_width, 0.5 * display_height, 0);
// Second pane node should be completely outside the view bounds
// along the z-axis and get clipped entirely.
scenic::ShapeNode pane_node2(session);
pane_node2.SetShape(scenic::Rectangle(session, pane_width, pane_height));
scenic::Material pane_material2(session);
pane_material2.SetColor(0, 255, 255, 255); // Another color.
pane_node2.SetMaterial(pane_material2);
pane_node2.SetTranslation(0.5 * pane_width, display_height - 0.5 * pane_height, 3);
test_session->scene.AddChild(view_holder);
view.AddChild(pane_node);
view.AddChild(pane_node2);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
scenic::Color unclipped_color = screenshot.ColorAt(0.1, 0.5);
scenic::Color clipped_color = screenshot.ColorAt(0.6, 0.5);
scenic::Color clipped_color2 = screenshot.ColorAt(0.1, 0.95);
// Unclipped color should be magenta, clipped should be black.
EXPECT_EQ(unclipped_color, scenic::Color(255, 0, 255, 255));
EXPECT_EQ(clipped_color, scenic::Color(0, 0, 0, 0));
// For pane2, it should be black as well.
EXPECT_EQ(clipped_color2, scenic::Color(0, 0, 0, 0));
}
// This unit test verifies the behavior of view bound clipping when the view exists under a node
// that itself has a translation applied to it. There are two views with a rectangle in each. The
// first view is under a node that is translated (display_width/2, 0,0). The second view is placed
// under the first transform node, and then translated again by (0, display_height/2, 0,0). This
// means that what you see on the screen should look like the following:
//
// xxxxxxxxxxvvvvvvvvvv
// xxxxxxxxxxvvvvvvvvvv
// xxxxxxxxxxvvvvvvvvvv
// xxxxxxxxxxvvvvvvvvvv
// xxxxxxxxxxvvvvvvvvvv
// xxxxxxxxxxrrrrrrrrrr
// xxxxxxxxxxrrrrrrrrrr
// xxxxxxxxxxrrrrrrrrrr
// xxxxxxxxxxrrrrrrrrrr
// xxxxxxxxxxrrrrrrrrrr
//
// Where x refers to empty display pixels.
// v refers to pixels covered by the first view's bounds.
// r refers to pixels covered by the second view's bounds.
//
// All of the view bounds are given in local coordinates (so their min-point is at (0,0) in the xy
// plane) which means the test would fail if the bounds were not being updated properly to the
// correct world-space location by the transform stack before rendering.
TEST_F(ScenicPixelTest, ViewBoundClippingWithTransforms) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
// Initialize second session
auto unique_session_2 = std::make_unique<scenic::Session>(scenic());
auto session2 = unique_session_2.get();
session2->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Session terminated.";
QuitLoop();
});
// Initialize third session
auto unique_session_3 = std::make_unique<scenic::Session>(scenic());
auto session3 = unique_session_3.get();
session3->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Session terminated.";
QuitLoop();
});
test_session->SetUpCamera().SetProjection(0);
// Add a transform node anchored in the top-middle of the display
// along the x-axis and at the top with respect to the y-axis.
scenic::EntityNode transform_node(session);
transform_node.SetTranslation(display_width / 2, 0, 0);
// Add the transform node as a child of the scene.
test_session->scene.AddChild(transform_node);
// Create two sets of view/view-holder token pairs.
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
auto [view_token_2, view_holder_token_2] = scenic::ViewTokenPair::New();
scenic::View view(session2, std::move(view_token), "ClipView");
scenic::ViewHolder view_holder(session, std::move(view_holder_token), "ClipViewHolder");
scenic::View view2(session3, std::move(view_token_2), "ClipView2");
scenic::ViewHolder view_holder2(session, std::move(view_holder_token_2), "ClipViewHolder2");
// Bounds of each view should be the size of a quarter of the display with
// origin at 0,0 relative to its transform node.
const std::array<float, 3> bmin = {0.f, 0.f, -2.f};
const std::array<float, 3> bmax = {display_width / 2, display_height / 2, 1.f};
const std::array<float, 3> imin = {0, 0, 0};
const std::array<float, 3> imax = {0, 0, 0};
view_holder.SetViewProperties(bmin, bmax, imin, imax);
view_holder2.SetViewProperties(bmin, bmax, imin, imax);
view_holder2.SetTranslation(0, display_height / 2, 0);
// Pane extends across the entire right-side of the display, even though
// its containing view is only in the top-right corner.
int32_t pane_width = display_width / 2;
int32_t pane_height = display_height;
scenic::Rectangle pane_shape(session2, pane_width, pane_height);
scenic::Rectangle pane_shape2(session3, pane_width, pane_height);
// Make two pane materials
scenic::Material pane_material(session2);
pane_material.SetColor(255, 0, 255, 255); // Magenta.
scenic::Material pane_material2(session3);
pane_material2.SetColor(0, 255, 255, 255); // Cyan
scenic::ShapeNode pane_node(session2);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation(pane_width / 2, pane_height / 2, 0);
scenic::ShapeNode pane_node2(session3);
pane_node2.SetShape(pane_shape2);
pane_node2.SetMaterial(pane_material2);
// Pane node 2 improperly extends above view2's bounds in the y-axis,
// overlapping with view1, but should still be clipped.
pane_node2.SetTranslation(pane_width / 2, 0, 0);
// Add view holders to the transform.
transform_node.AddChild(view_holder);
view.AddChild(pane_node);
transform_node.AddChild(view_holder2);
view2.AddChild(pane_node2);
Present(session);
Present(session2);
Present(session3);
scenic::Screenshot screenshot = TakeScreenshot();
scenic::Color magenta_color = screenshot.ColorAt(0.6, 0.1);
scenic::Color magenta_color2 = screenshot.ColorAt(0.9, 0.4);
scenic::Color cyan_color = screenshot.ColorAt(0.6, 0.9);
scenic::Color black_color = screenshot.ColorAt(0.0, 0.5);
// Upper-right quadrant should be magenta, lower-right quadrant
// should be cyan. The left half of the screen should be black.
EXPECT_EQ(magenta_color, scenic::Color(255, 0, 255, 255));
EXPECT_EQ(magenta_color2, scenic::Color(255, 0, 255, 255));
EXPECT_EQ(cyan_color, scenic::Color(0, 255, 255, 255));
EXPECT_EQ(black_color, scenic::Color(0, 0, 0, 0));
}
// Creates three views and renders their wireframe bounds.
// Looks like this:
//
// aaaaaaaaaabbbbbbbbbb
// a ab b
// a ab b
// a abbbbbbbbbb
// a acccccccccc
// a ac c
// a ac c
// aaaaaaaaaacccccccccc
//
// Where a,b, and c represent the bounds for views 1,2, and
// 3 respectively.
TEST_F(ScenicPixelTest, ViewBoundWireframeRendering) {
auto escher = escher::test::GetEscher()->GetWeakPtr();
bool supports_wireframe = escher->supports_wireframe();
if (!supports_wireframe) {
FX_LOGS(INFO) << "Vulkan device feature fillModeNonSolid is not supported on this device. "
"Error messages are expected.";
}
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
test_session->SetUpCamera().SetProjection(0);
// Initialize session 2.
auto unique_session2 = std::make_unique<scenic::Session>(scenic());
auto session2 = unique_session2.get();
session2->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Session terminated.";
QuitLoop();
});
// Initialize session 3.
auto unique_session3 = std::make_unique<scenic::Session>(scenic());
auto session3 = unique_session3.get();
session3->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Session terminated.";
QuitLoop();
});
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
auto [view_token2, view_holder_token2] = scenic::ViewTokenPair::New();
auto [view_token3, view_holder_token3] = scenic::ViewTokenPair::New();
scenic::View view(session, std::move(view_token), "ClipView");
scenic::ViewHolder view_holder(session, std::move(view_holder_token), "ClipViewHolder");
// View 2 is embedded by view 1.
scenic::View view2(session2, std::move(view_token2), "ClipView2");
scenic::ViewHolder view_holder2(session, std::move(view_holder_token2), "ClipViewHolder2");
// View 3 is embedded by view 2 and thus doubly embedded within view 1.
scenic::View view3(session3, std::move(view_token3), "ClipView3");
scenic::ViewHolder view_holder3(session2, std::move(view_holder_token3), "ClipViewHolder3");
const std::array<float, 3> bmin = {0.f, 0.f, -2.f};
const std::array<float, 3> bmax = {display_width / 2, display_height, 1.f};
const std::array<float, 3> imin = {1, 1, 0};
const std::array<float, 3> imax = {1, 1, 0};
view_holder.SetViewProperties(bmin, bmax, imin, imax);
const std::array<float, 3> bmin2 = {0, 0, -2.f};
const std::array<float, 3> bmax2 = {display_width / 2, display_height / 2, 1.f};
view_holder2.SetViewProperties(bmin2, bmax2, imin, imax);
view_holder3.SetViewProperties(bmin2, bmax2, imin, imax);
// Set the debug bounds colors.
view_holder.SetDebugBoundsColor(0, 255, 255);
view_holder2.SetDebugBoundsColor(255, 0, 255);
view_holder3.SetDebugBoundsColor(255, 255, 0);
// Set bounds rendering on just the first view. This should turn on debug
// wireframe for itself and view2, since view2 is a direct embedding. View3
// should still be off.
view.enableDebugBounds(true);
scene->AddChild(view_holder);
// Transform and embed view holder 2 in first view.
scenic::EntityNode transform_node(session);
transform_node.SetTranslation(display_width / 2, 0, 0);
view.AddChild(transform_node);
transform_node.AddChild(view_holder2);
// Transform and embed view holder 3 in view 2.
scenic::EntityNode transform_node2(session2);
transform_node2.SetTranslation(0, display_height / 2, 0);
view2.AddChild(transform_node2);
transform_node2.AddChild(view_holder3);
Present(session);
Present(session2);
Present(session3);
// Take screenshot.
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
auto histogram = screenshot.Histogram();
if (supports_wireframe) {
histogram.erase({0, 0, 0, 0});
scenic::Color expected_colors[2] = {{0, 255, 255, 255}, // First ViewHolder
{255, 0, 255, 255}}; // Second ViewHolder
for (uint32_t i = 0; i < 2; i++) {
EXPECT_GT(histogram[expected_colors[i]], 0u);
histogram.erase(expected_colors[i]);
}
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
} else {
// If drawing wireframe is not supported, there should be nothing displayed
// on screen.
histogram.erase({0, 0, 0, 0});
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
// Now toggle debug rendering for view 2. This should tirgger view3's bounds to
// display as view3 is directly embedded by view2.
view2.enableDebugBounds(true);
Present(session);
Present(session2);
Present(session3);
// Take screenshot.
scenic::Screenshot screenshot2 = TakeScreenshot();
ASSERT_FALSE(screenshot2.empty());
histogram = screenshot2.Histogram();
if (supports_wireframe) {
histogram.erase({0, 0, 0, 0});
scenic::Color expected_colors_2[3] = {{0, 255, 255, 255}, // First ViewHolder
{255, 0, 255, 255}, // Second ViewHolder
{255, 255, 0, 255}}; // Third ViewHolder
for (uint32_t i = 0; i < 3; i++) {
EXPECT_GT(histogram[expected_colors_2[i]], 0u);
histogram.erase(expected_colors_2[i]);
}
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
} else {
// If drawing wireframe is not supported, there should be nothing displayed
// on screen.
histogram.erase({0, 0, 0, 0});
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
}
// TODO(SCN-1375): Blocked against hardware inability
// to provide accurate screenshots from the physical
// display. Our "TakeScreenshot()" method only grabs
// pixel data from Escher before it gets sent off to
// the display controller and thus cannot accurately
// capture color conversion information.
TEST_F(ScenicPixelTest, DISABLED_Compositor) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
test_session->SetUpCamera().SetProjection(0);
// Color correction data
static const std::array<float, 3> preoffsets = {0, 0, 0};
static const std::array<float, 9> matrix = {.288299, 0.052709, -0.257912, 0.711701, 0.947291,
0.257912, 0.000000, -0.000000, 1.000000};
static const std::array<float, 3> postoffsets = {0, 0, 0};
static const glm::mat4 glm_matrix(.288299, 0.052709, -0.257912, 0.00000, 0.711701, 0.947291,
0.257912, 0.00000, 0.000000, -0.000000, 1.000000, 0.00000,
0.000000, 0.000000, 0.00000, 1.00000);
const float pane_width = display_width / 5;
const float pane_height = display_height;
static const float colors[15] = {1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0};
for (uint32_t i = 0; i < 5; i++) {
scenic::Rectangle pane_shape(session, pane_width, pane_height);
scenic::Material pane_material(session);
pane_material.SetColor(255 * colors[3 * i], 255 * colors[3 * i + 1], 255 * colors[3 * i + 2],
255);
scenic::ShapeNode pane_node(session);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation((i + 0.5) * pane_width, 0.5 * pane_height, -20);
scene->AddChild(pane_node);
}
// Display uncorrected version first.
Present(session);
scenic::Screenshot prev_screenshot = TakeScreenshot();
// Apply color correction.
test_session->compositor.SetColorConversion(preoffsets, matrix, postoffsets);
// Display color corrected version.
Present(session, zx::time(1000000));
scenic::Screenshot post_screenshot = TakeScreenshot();
for (uint32_t i = 0; i < 5; i++) {
scenic::Color prev_color = prev_screenshot.ColorAt(i * .2, 0.5);
scenic::Color post_color = post_screenshot.ColorAt(i * .2, 0.5);
glm::vec4 vec = glm_matrix * glm::vec4(prev_color.r, prev_color.g, prev_color.b, 1);
scenic::Color res(vec.x, vec.y, vec.z, vec.w);
EXPECT_EQ(res, post_color);
}
}
// This test sets up a scene, takes a screenshot, rotates display configuration
// by 90 degrees and takes a second screenshot, then makes sure that the pixels
// in both screenshots map onto each other how you would expect.
class RotationTest : public ScenicPixelTest {
public:
void TestRotation(uint32_t angle) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
test_session->SetUpCamera().SetProjection(0);
const float pane_width = display_width / 5;
const float pane_height = display_height;
// For this test, create 5 vertical bands. This is an array of
// the rgb colors for each of the five bands that will be
// created below.
static const float colors[15] = {1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0};
for (uint32_t i = 0; i < 5; i++) {
scenic::Rectangle pane_shape(session, pane_width, pane_height);
scenic::Material pane_material(session);
pane_material.SetColor(255 * colors[3 * i], 255 * colors[3 * i + 1], 255 * colors[3 * i + 2],
255);
scenic::ShapeNode pane_node(session);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation((i + 0.5) * pane_width, 0.5 * pane_height, -20);
scene->AddChild(pane_node);
}
// Display unrotated version first.
Present(session);
scenic::Screenshot prev_screenshot = TakeScreenshot();
test_session->compositor.SetLayoutRotation(angle);
// Display rotated version.
Present(session, zx::time(1000000));
scenic::Screenshot post_screenshot = TakeScreenshot();
// The pre and post width and height should be the reverse of each other.
EXPECT_EQ(prev_screenshot.width(), post_screenshot.height());
EXPECT_EQ(prev_screenshot.height(), post_screenshot.width());
// All of the colors should be transposed.
// Only support 90 and 270 degree rotations here.
for (uint32_t x = 0; x < prev_screenshot.width(); x++) {
for (uint32_t y = 0; y < prev_screenshot.height(); y++) {
uint32_t post_x = angle == 90 ? y : prev_screenshot.height() - y - 1;
uint32_t post_y = angle == 90 ? prev_screenshot.width() - x - 1 : x;
EXPECT_EQ(prev_screenshot[y][x], post_screenshot[post_y][post_x]);
}
}
}
};
TEST_F(RotationTest, Test90) { TestRotation(90); }
TEST_F(RotationTest, RotationTest270) { TestRotation(270); }
// Test to make sure scenic can properly render basic shapes like circles.
TEST_F(ScenicPixelTest, BasicShapeTest) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
test_session->SetUpCamera().SetProjection(0);
const float kRadius = 10;
scenic::Circle circle_shape(session, kRadius);
scenic::Material circle_material(session);
circle_material.SetColor(255, 0, 255, 255);
scenic::ShapeNode circle_node(session);
circle_node.SetShape(circle_shape);
circle_node.SetMaterial(circle_material);
circle_node.SetTranslation(display_width / 2, display_height / 2, -20);
scene->AddChild(circle_node);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
EXPECT_EQ(screenshot.ColorAt(0.5, 0.5), scenic::Color(255, 0, 255, 255));
}
// This test zooms in on the lower-right quadrant and verifies that only that is
// shown.
TEST_F(ScenicPixelTest, ClipSpaceTransformOrtho) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
struct Shape {
float scale;
scenic::Color color;
glm::vec3 translation;
};
// clang-format off
static const std::array<Shape, 3> shapes {
Shape {
.scale = 1,
.color = {255, 0, 0, 255},
.translation = {.5f, .5f, -10}
},
Shape {
.scale = .5f,
.color = {0, 255, 0, 255},
.translation = {.75f, .75f, -20}
},
Shape {
.scale = .4f,
.color = {0, 0, 255, 255},
.translation = {.75f, .75f, -30}
}
};
// clang-format on
for (const auto& shape : shapes) {
scenic::Rectangle rectangle(session, shape.scale * display_width, shape.scale * display_height);
scenic::Material material(session);
material.SetColor(shape.color.r, shape.color.g, shape.color.b, shape.color.a);
scenic::ShapeNode node(session);
node.SetShape(rectangle);
node.SetMaterial(material);
node.SetTranslation(shape.translation.x * display_width, shape.translation.y * display_height,
shape.translation.z);
scene->AddChild(node);
}
auto camera = test_session->SetUpCamera();
camera.SetProjection(0);
camera.SetClipSpaceTransform(-1, -1, 2);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_EQ(histogram[shapes[0].color], 0u);
EXPECT_GT(histogram[shapes[1].color], 0u);
EXPECT_GT(histogram[shapes[2].color], histogram[shapes[1].color]);
}
// This test ensures that clip-space transforms do not distort the projection
// matrix by setting up a scene that contains a splitting plane that should not
// show up in perspective (aligned with the view vector, centered) but would if
// the camera were naively translated.
//
// Viewed from above, the scene looks like this:
// bad good
// \ b /
// ?\ a /?
// ??\d/??
// cam
// zoom (2x, right side)
TEST_F(ScenicPixelTest, ClipSpaceTransformPerspective) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
scenic::Scene* const scene = &test_session->scene;
static const glm::quat face_right = glm::angleAxis(kPi / 2, glm::vec3(0, -1, 0));
static const float kFovy = kPi / 4;
static const float background_height =
2 * tan(kFovy / 2) * gfx::TestSession::kDefaultCameraOffset;
const float background_width = background_height / display_height * display_width;
struct Shape {
scenic::Color color;
glm::vec2 size;
glm::vec3 translation;
const glm::quat* rotation;
};
// clang-format off
static const std::array<Shape, 3> shapes {
Shape {
.color = {255, 0, 0, 255},
.size = {background_width / 2, background_height},
.translation = {-background_width / 4, 0, -10},
.rotation = nullptr
},
Shape {
.color = {0, 255, 0, 255},
.size = {background_width / 2, background_height},
.translation = {background_width / 4, 0, -10},
.rotation = nullptr
},
Shape {
.color = {0, 0, 255, 255},
// SCN-1276: The depth of the viewing volume is 1000.
.size = {1000, background_height},
.translation = {0, 0, -500},
.rotation = &face_right
}
};
// clang-format on
for (const auto& shape : shapes) {
scenic::Rectangle rectangle(session, shape.size.x, shape.size.y);
scenic::Material material(session);
material.SetColor(shape.color.r, shape.color.g, shape.color.b, shape.color.a);
scenic::ShapeNode node(session);
node.SetShape(rectangle);
node.SetMaterial(material);
node.SetTranslation(shape.translation.x + display_width / 2,
shape.translation.y + display_height / 2, shape.translation.z);
if (shape.rotation) {
node.SetRotation(shape.rotation->x, shape.rotation->y, shape.rotation->z, shape.rotation->w);
}
scene->AddChild(node);
}
auto camera = test_session->SetUpCamera();
camera.SetProjection(kFovy);
camera.SetClipSpaceTransform(-1, 0, 2);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_EQ(histogram[shapes[0].color], 0u);
EXPECT_EQ(histogram[shapes[2].color], 0u);
EXPECT_GT(histogram[shapes[1].color], 0u);
}
class ParameterizedYuvPixelTest
: public ScenicPixelTest,
public ::testing::WithParamInterface<fuchsia::sysmem::PixelFormatType> {};
// Test that exercies sampling from YUV textures.
TEST_P(ParameterizedYuvPixelTest, YuvImagesOnImagePipe2) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
test_session->SetUpCamera().SetProjection(0);
uint32_t next_id = session->next_resource_id();
fuchsia::images::ImagePipe2Ptr image_pipe;
const uint32_t kImagePipeId = next_id++;
session->Enqueue(scenic::NewCreateImagePipe2Cmd(kImagePipeId, image_pipe.NewRequest()));
const uint32_t kMaterialId = next_id++;
session->Enqueue(scenic::NewCreateMaterialCmd(kMaterialId));
session->Enqueue(scenic::NewSetTextureCmd(kMaterialId, kImagePipeId));
const uint32_t kShapeNodeId = next_id++;
session->Enqueue(scenic::NewCreateShapeNodeCmd(kShapeNodeId));
session->Enqueue(scenic::NewSetMaterialCmd(kShapeNodeId, kMaterialId));
const uint32_t kShapeId = next_id++;
session->Enqueue(scenic::NewCreateRectangleCmd(kShapeId, display_width, display_height));
session->Enqueue(scenic::NewSetShapeCmd(kShapeNodeId, kShapeId));
session->Enqueue(
scenic::NewSetTranslationCmd(kShapeNodeId, {display_width * 0.5f, display_height * 0.5f, 0}));
session->Enqueue(scenic::NewAddChildCmd(test_session->scene.id(), kShapeNodeId));
Present(session);
const uint32_t kShapeWidth = 32;
const uint32_t kShapeHeight = 32;
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator;
zx_status_t status = fdio_service_connect("/svc/fuchsia.sysmem.Allocator",
sysmem_allocator.NewRequest().TakeChannel().release());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionTokenSyncPtr local_token;
status = sysmem_allocator->AllocateSharedCollection(local_token.NewRequest());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionTokenSyncPtr dup_token;
status = local_token->Duplicate(std::numeric_limits<uint32_t>::max(), dup_token.NewRequest());
EXPECT_EQ(status, ZX_OK);
status = local_token->Sync();
EXPECT_EQ(status, ZX_OK);
const uint32_t kBufferId = 1;
image_pipe->AddBufferCollection(kBufferId, std::move(dup_token));
fuchsia::sysmem::BufferCollectionSyncPtr buffer_collection;
status = sysmem_allocator->BindSharedCollection(std::move(local_token),
buffer_collection.NewRequest());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionConstraints constraints;
constraints.has_buffer_memory_constraints = true;
constraints.buffer_memory_constraints.cpu_domain_supported = true;
constraints.buffer_memory_constraints.ram_domain_supported = true;
constraints.usage.cpu = fuchsia::sysmem::cpuUsageWriteOften;
constraints.image_format_constraints_count = 1;
auto& image_constraints = constraints.image_format_constraints[0];
image_constraints.pixel_format.type = GetParam();
image_constraints.color_spaces_count = 1;
image_constraints.color_space[0] =
fuchsia::sysmem::ColorSpace{.type = fuchsia::sysmem::ColorSpaceType::REC709};
image_constraints.min_coded_width = kShapeWidth;
image_constraints.max_coded_width = kShapeWidth;
image_constraints.min_coded_height = kShapeHeight;
image_constraints.max_coded_height = kShapeHeight;
image_constraints.max_bytes_per_row = kShapeWidth;
status = buffer_collection->SetConstraints(true, constraints);
EXPECT_EQ(status, ZX_OK);
zx_status_t allocation_status = ZX_OK;
fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info = {};
status = buffer_collection->WaitForBuffersAllocated(&allocation_status, &buffer_collection_info);
EXPECT_EQ(status, ZX_OK);
// TODO(54153): This test is skipped on FEMU until we support external
// host-visible image allocation on FEMU.
if (allocation_status == ZX_ERR_NOT_SUPPORTED) {
FX_LOGS(WARNING) << "Buffer constraints not supported. Test skipped.";
GTEST_SKIP();
} else {
ASSERT_EQ(ZX_OK, allocation_status);
}
EXPECT_FALSE(buffer_collection_info.settings.buffer_settings.is_secure);
status = buffer_collection->Close();
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::ImageFormat_2 image_format = {};
image_format.coded_width = kShapeWidth;
image_format.coded_height = kShapeHeight;
const uint32_t kImageId = 1;
image_pipe->AddImage(kImageId, kBufferId, 0, image_format);
uint8_t* vmo_base;
const zx::vmo& image_vmo = buffer_collection_info.buffers[0].vmo;
auto image_vmo_bytes = buffer_collection_info.settings.buffer_settings.size_bytes;
EXPECT_GT(image_vmo_bytes, 0u);
status = zx::vmar::root_self()->map(0, image_vmo, 0, image_vmo_bytes,
ZX_VM_PERM_WRITE | ZX_VM_PERM_READ,
reinterpret_cast<uintptr_t*>(&vmo_base));
vmo_base += buffer_collection_info.buffers[0].vmo_usable_start;
const uint32_t num_pixels = kShapeWidth * kShapeHeight;
static const uint8_t kYValue = 110;
static const uint8_t kUValue = 192;
static const uint8_t kVValue = 192;
const scenic::Color kBgraColor = {0xF1, 0x87, 0xFA, 0xFF};
for (uint32_t i = 0; i < num_pixels; ++i) {
vmo_base[i] = kYValue;
}
switch (GetParam()) {
case fuchsia::sysmem::PixelFormatType::NV12:
for (uint32_t i = num_pixels; i < num_pixels + num_pixels / 2; i += 2) {
vmo_base[i] = kUValue;
vmo_base[i + 1] = kVValue;
}
break;
case fuchsia::sysmem::PixelFormatType::I420:
for (uint32_t i = num_pixels; i < num_pixels + num_pixels / 4; ++i) {
vmo_base[i] = kUValue;
}
for (uint32_t i = num_pixels + num_pixels / 4; i < num_pixels + num_pixels / 2; ++i) {
vmo_base[i] = kVValue;
}
break;
default:
FX_NOTREACHED();
}
if (buffer_collection_info.settings.buffer_settings.coherency_domain ==
fuchsia::sysmem::CoherencyDomain::RAM) {
EXPECT_EQ(ZX_OK, buffer_collection_info.buffers[0].vmo.op_range(ZX_VMO_OP_CACHE_CLEAN, 0,
image_vmo_bytes, nullptr, 0));
}
bool image_presented = false;
image_pipe->PresentImage(
kImageId, zx_clock_get_monotonic(), std::vector<zx::event>(), std::vector<zx::event>(),
[&image_presented](fuchsia::images::PresentationInfo pinfo) { image_presented = true; });
// Ensure an image with contents will be presented to the screen.
EXPECT_TRUE(
RunLoopWithTimeoutOrUntil([&image_presented] { return image_presented; }, zx::sec(15)));
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
// Check that all pixels have the expected color.
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[kBgraColor], 0u);
histogram.erase(kBgraColor);
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
INSTANTIATE_TEST_SUITE_P(YuvPixelFormats, ParameterizedYuvPixelTest,
::testing::Values(fuchsia::sysmem::PixelFormatType::NV12,
fuchsia::sysmem::PixelFormatType::I420));
// We cannot capture protected content, so we expect a fuchsia screenshot instead.
TEST_F(ScenicPixelTest, ProtectedImage) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
test_session->SetUpCamera().SetProjection(0);
fuchsia::images::ImagePipe2Ptr image_pipe;
image_pipe.set_error_handler([](zx_status_t status) { GTEST_FAIL() << "ImagePipe terminated."; });
const uint32_t kImagePipeId = session->next_resource_id();
session->Enqueue(scenic::NewCreateImagePipe2Cmd(kImagePipeId, image_pipe.NewRequest()));
const uint32_t kMaterialId = kImagePipeId + 1;
session->Enqueue(scenic::NewCreateMaterialCmd(kMaterialId));
session->Enqueue(scenic::NewSetTextureCmd(kMaterialId, kImagePipeId));
const uint32_t kShapeNodeId = kMaterialId + 1;
session->Enqueue(scenic::NewCreateShapeNodeCmd(kShapeNodeId));
session->Enqueue(scenic::NewSetMaterialCmd(kShapeNodeId, kMaterialId));
const uint32_t kShapeId = kShapeNodeId + 1;
session->Enqueue(scenic::NewCreateRectangleCmd(kShapeId, display_width, display_height));
session->Enqueue(scenic::NewSetShapeCmd(kShapeNodeId, kShapeId));
session->Enqueue(scenic::NewAddChildCmd(test_session->scene.id(), kShapeNodeId));
Present(session);
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator;
zx_status_t status = fdio_service_connect("/svc/fuchsia.sysmem.Allocator",
sysmem_allocator.NewRequest().TakeChannel().release());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionTokenSyncPtr local_token;
status = sysmem_allocator->AllocateSharedCollection(local_token.NewRequest());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionTokenSyncPtr dup_token;
status = local_token->Duplicate(std::numeric_limits<uint32_t>::max(), dup_token.NewRequest());
EXPECT_EQ(status, ZX_OK);
status = local_token->Sync();
EXPECT_EQ(status, ZX_OK);
ASSERT_TRUE(image_pipe.is_bound());
const uint32_t kBufferId = 1;
image_pipe->AddBufferCollection(kBufferId, std::move(dup_token));
// WaitForBuffersAllocated() hangs if AddBufferCollection() isn't finished successfully.
RunLoopUntilIdle();
fuchsia::sysmem::BufferCollectionSyncPtr buffer_collection;
status = sysmem_allocator->BindSharedCollection(std::move(local_token),
buffer_collection.NewRequest());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionConstraints constraints;
constraints.has_buffer_memory_constraints = true;
constraints.buffer_memory_constraints.secure_required = true;
constraints.buffer_memory_constraints.inaccessible_domain_supported = true;
constraints.buffer_memory_constraints.cpu_domain_supported = false;
constraints.buffer_memory_constraints.ram_domain_supported = false;
constraints.usage.vulkan = fuchsia::sysmem::vulkanUsageTransferSrc;
constraints.image_format_constraints_count = 1;
auto& image_constraints = constraints.image_format_constraints[0];
image_constraints.pixel_format.type = fuchsia::sysmem::PixelFormatType::BGRA32;
image_constraints.color_spaces_count = 1;
image_constraints.color_space[0] =
fuchsia::sysmem::ColorSpace{.type = fuchsia::sysmem::ColorSpaceType::SRGB};
status = buffer_collection->SetConstraints(true, constraints);
EXPECT_EQ(status, ZX_OK);
zx_status_t allocation_status = ZX_OK;
fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info = {};
status = buffer_collection->WaitForBuffersAllocated(&allocation_status, &buffer_collection_info);
if (allocation_status != ZX_OK) {
// Protected memory might not be available in some devices which causes allocation failure.
GTEST_SKIP() << "Protected memory cannot be allocated";
}
EXPECT_EQ(status, ZX_OK);
EXPECT_TRUE(buffer_collection_info.settings.buffer_settings.is_secure);
EXPECT_EQ(
0u,
fsl::GetObjectName(buffer_collection_info.buffers[0].vmo.get()).find("ImagePipe2Surface"));
status = buffer_collection->Close();
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::ImageFormat_2 image_format = {};
image_format.coded_width = 1;
image_format.coded_height = 1;
const uint32_t kImageId = 1;
image_pipe->AddImage(kImageId, kBufferId, 0, image_format);
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
EXPECT_EQ(scenic::Color({255, 0, 255, 255}), screenshot.ColorAt(.25f, .25f));
}
// Flaking on bots. TODO(fxb/42892): Re-enable. Add all supported pixel formats as test cases.
TEST_F(ScenicPixelTest, DISABLED_LinearImagePipe) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
test_session->SetUpCamera().SetProjection(0);
fuchsia::images::ImagePipe2Ptr image_pipe;
const uint32_t kImagePipeId = session->next_resource_id();
session->Enqueue(scenic::NewCreateImagePipe2Cmd(kImagePipeId, image_pipe.NewRequest()));
const uint32_t kMaterialId = kImagePipeId + 1;
session->Enqueue(scenic::NewCreateMaterialCmd(kMaterialId));
session->Enqueue(scenic::NewSetTextureCmd(kMaterialId, kImagePipeId));
const uint32_t kShapeNodeId = kMaterialId + 1;
session->Enqueue(scenic::NewCreateShapeNodeCmd(kShapeNodeId));
session->Enqueue(scenic::NewSetMaterialCmd(kShapeNodeId, kMaterialId));
const uint32_t kShapeId = kShapeNodeId + 1;
session->Enqueue(scenic::NewCreateRectangleCmd(kShapeId, display_width, display_height));
session->Enqueue(scenic::NewSetShapeCmd(kShapeNodeId, kShapeId));
session->Enqueue(scenic::NewAddChildCmd(test_session->scene.id(), kShapeNodeId));
Present(session);
fuchsia::sysmem::AllocatorSyncPtr sysmem_allocator;
zx_status_t status = fdio_service_connect("/svc/fuchsia.sysmem.Allocator",
sysmem_allocator.NewRequest().TakeChannel().release());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionTokenSyncPtr local_token;
status = sysmem_allocator->AllocateSharedCollection(local_token.NewRequest());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionTokenSyncPtr dup_token;
status = local_token->Duplicate(std::numeric_limits<uint32_t>::max(), dup_token.NewRequest());
EXPECT_EQ(status, ZX_OK);
status = local_token->Sync();
EXPECT_EQ(status, ZX_OK);
const uint32_t kBufferId = 1;
image_pipe->AddBufferCollection(kBufferId, std::move(dup_token));
fuchsia::sysmem::BufferCollectionSyncPtr buffer_collection;
status = sysmem_allocator->BindSharedCollection(std::move(local_token),
buffer_collection.NewRequest());
EXPECT_EQ(status, ZX_OK);
fuchsia::sysmem::BufferCollectionConstraints constraints;
constraints.has_buffer_memory_constraints = true;
constraints.buffer_memory_constraints.cpu_domain_supported = true;
constraints.buffer_memory_constraints.ram_domain_supported = true;
constraints.usage.cpu = fuchsia::sysmem::cpuUsageWriteOften;
constraints.image_format_constraints_count = 1;
auto& image_constraints = constraints.image_format_constraints[0];
image_constraints.color_spaces_count = 1;
image_constraints.color_space[0] =
fuchsia::sysmem::ColorSpace{.type = fuchsia::sysmem::ColorSpaceType::SRGB};
image_constraints.pixel_format.type = fuchsia::sysmem::PixelFormatType::BGRA32;
image_constraints.pixel_format.has_format_modifier = true;
image_constraints.pixel_format.format_modifier.value = fuchsia::sysmem::FORMAT_MODIFIER_LINEAR;
image_constraints.required_max_coded_width = 1;
image_constraints.required_max_coded_height = 1;
status = buffer_collection->SetConstraints(true, constraints);
EXPECT_EQ(status, ZX_OK);
zx_status_t allocation_status = ZX_OK;
fuchsia::sysmem::BufferCollectionInfo_2 buffer_collection_info = {};
status = buffer_collection->WaitForBuffersAllocated(&allocation_status, &buffer_collection_info);
EXPECT_EQ(ZX_OK, allocation_status);
EXPECT_EQ(status, ZX_OK);
EXPECT_FALSE(buffer_collection_info.settings.buffer_settings.is_secure);
status = buffer_collection->Close();
EXPECT_EQ(status, ZX_OK);
// R=255 G=0 B=255 in BGRA32
constexpr uint32_t kColor = 0xffff00ff;
uint32_t kPixelSize = 4;
EXPECT_EQ(ZX_OK, buffer_collection_info.buffers[0].vmo.write(&kColor, 0, kPixelSize));
if (buffer_collection_info.settings.buffer_settings.coherency_domain ==
fuchsia::sysmem::CoherencyDomain::RAM) {
EXPECT_EQ(ZX_OK, buffer_collection_info.buffers[0].vmo.op_range(ZX_VMO_OP_CACHE_CLEAN, 0,
kPixelSize, nullptr, 0));
}
fuchsia::sysmem::ImageFormat_2 image_format = {};
image_format.coded_width = 1;
image_format.coded_height = 1;
const uint32_t kImageId = 1;
image_pipe->AddImage(kImageId, kBufferId, 0, image_format);
image_pipe->PresentImage(kImageId, zx_clock_get_monotonic(), std::vector<zx::event>(),
std::vector<zx::event>(),
[this](fuchsia::images::PresentationInfo pinfo) { this->QuitLoop(); });
// Ensure an image with contents will be presented to the screen.
ASSERT_FALSE(RunLoopWithTimeout(zx::sec(15)));
Present(session);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
EXPECT_EQ(scenic::Color({255, 0, 255, 255}), screenshot.ColorAt(.25f, .25f));
}
// This test ensures that detaching a view holder ceases rendering the view. Finer grained
// functionality is covered in node and view unit tests.
TEST_F(ScenicPixelTest, ViewHolderDetach) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
test_session->SetUpCamera().SetProjection(0);
auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
scenic::View view(session, std::move(view_token), "View");
scenic::ViewHolder view_holder(session, std::move(view_holder_token), "ViewHolder");
view_holder.SetViewProperties({.bounding_box = {
.min = {0, 0, -2},
.max = {display_width, display_height, 1},
}});
// Solid color
scenic::Rectangle pane_shape(session, display_width, display_height);
scenic::Material pane_material(session);
pane_material.SetColor(255, 0, 255, 255); // Magenta.
scenic::ShapeNode pane_node(session);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material);
pane_node.SetTranslation(display_width / 2, display_height / 2, 0);
test_session->scene.AddChild(view_holder);
view.AddChild(pane_node);
Present(session);
EXPECT_EQ(TakeScreenshot().ColorAt(.5f, .5f), scenic::Color(255, 0, 255, 255)); // Magenta
view_holder.Detach();
Present(session);
EXPECT_EQ(TakeScreenshot().ColorAt(.5f, .5f), scenic::Color(0, 0, 0, 0)); // Blank
}
// This test case tests if Scenic can generate and present external GPU images correctly without
// causing any Vulkan validation errors (fxb/35652).
//
// This test first creates an escher Image and GPU memory bound to it, and uploaded plain color
// pixels (#FF8000) to that image. Then we export image as a vmo object, create that image using vmo
// directly in Scenic, and present that image.
//
// The image layout type should be correctly converted from eUndefined or ePreinitialized to any
// other valid type when it is presented. Otherwise this test will crash due to validation errors in
// gfx system.
VK_TEST_F(ScenicPixelTest, UseExternalImage) {
constexpr size_t kImageSize = 256;
constexpr scenic::Color kImageColor = {255, 128, 0, 255};
scenic::BackgroundView view(CreatePresentationContext());
auto escher_ptr = escher::test::GetEscher()->GetWeakPtr();
auto uploader = escher::BatchGpuUploader::New(escher_ptr);
// Create a RGBA (8-bit channels) images to write to.
escher::ImageInfo image_info = {
.format = vk::Format::eB8G8R8A8Unorm,
.width = kImageSize,
.height = kImageSize,
.sample_count = 1,
.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eSampled,
.memory_flags = vk::MemoryPropertyFlagBits::eDeviceLocal,
.tiling = vk::ImageTiling::eOptimal,
.is_mutable = true,
.is_external = true};
auto [gpu_mem_ptr, image] = escher::GenerateExportableMemImage(
escher_ptr->vk_device(), escher_ptr->resource_recycler(), image_info);
ASSERT_NE(gpu_mem_ptr.get(), nullptr);
// Create and upload pixels to the escher Image.
auto pixels = std::vector<uint8_t>(image_info.height * image_info.width * 4U);
for (uint32_t i = 0; i < image_info.height * image_info.width; ++i) {
pixels[i * 4] = kImageColor.b;
pixels[i * 4 + 1] = kImageColor.g;
pixels[i * 4 + 2] = kImageColor.r;
pixels[i * 4 + 3] = kImageColor.a;
}
// Write the pixels generated above to escher Image.
escher::image_utils::WritePixelsToImage(uploader.get(), pixels.data(), image);
uploader->Submit();
escher_ptr->vk_device().waitIdle();
// Export the escher image as vmo for GpuImage creation.
zx::vmo image_vmo = escher::ExportMemoryAsVmo(escher_ptr.get(), gpu_mem_ptr);
uint64_t vmo_size = 0;
auto get_size_result = image_vmo.get_size(&vmo_size);
ASSERT_TRUE(get_size_result == ZX_OK);
// Create a GPU image using the vmo exported above.
fuchsia::images::ImageInfo fx_image_info{
.width = kImageSize,
.height = kImageSize,
.stride = static_cast<uint32_t>(
kImageSize * images::StrideBytesPerWidthPixel(fuchsia::images::PixelFormat::BGRA_8)),
.pixel_format = fuchsia::images::PixelFormat::BGRA_8,
.tiling = fuchsia::images::Tiling::GPU_OPTIMAL,
};
// Present the external GPU image using BackgroundView.
view.SetImage(std::move(image_vmo), vmo_size, fx_image_info,
fuchsia::images::MemoryType::VK_DEVICE_MEMORY);
RunUntilIndirectPresent(&view);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[kImageColor], 0u);
histogram.erase(kImageColor);
// This assert is written this way so that, when it fails, it prints out all
// the unexpected colors
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
VK_TEST_F(ScenicPixelTest, UseExternalImageImmutableRgba) {
constexpr size_t kImageSize = 256;
constexpr scenic::Color kImageColor = {255, 128, 0, 255};
scenic::BackgroundView view(CreatePresentationContext());
auto escher_ptr = escher::test::GetEscher()->GetWeakPtr();
auto uploader = escher::BatchGpuUploader::New(escher_ptr);
// Create a RGBA (8-bit channels) images to write to.
escher::ImageInfo image_info = {
// SRGB is required for immutable external images.
.format = vk::Format::eR8G8B8A8Srgb,
.width = kImageSize,
.height = kImageSize,
.sample_count = 1,
.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eSampled,
.memory_flags = vk::MemoryPropertyFlagBits::eDeviceLocal,
.tiling = vk::ImageTiling::eOptimal,
.is_mutable = false,
.is_external = true};
auto [gpu_mem_ptr, image] = escher::GenerateExportableMemImage(
escher_ptr->vk_device(), escher_ptr->resource_recycler(), image_info);
ASSERT_NE(gpu_mem_ptr.get(), nullptr);
// Create and upload pixels to the escher Image.
auto pixels = std::vector<uint8_t>(image_info.height * image_info.width * 4U);
for (uint32_t i = 0; i < image_info.height * image_info.width; ++i) {
pixels[i * 4] = kImageColor.r;
pixels[i * 4 + 1] = kImageColor.g;
pixels[i * 4 + 2] = kImageColor.b;
pixels[i * 4 + 3] = kImageColor.a;
}
// Write the pixels generated above to escher Image.
escher::image_utils::WritePixelsToImage(uploader.get(), pixels.data(), image);
uploader->Submit();
escher_ptr->vk_device().waitIdle();
// Export the escher image as vmo for GpuImage creation.
zx::vmo image_vmo = escher::ExportMemoryAsVmo(escher_ptr.get(), gpu_mem_ptr);
uint64_t vmo_size = 0;
auto get_size_result = image_vmo.get_size(&vmo_size);
ASSERT_TRUE(get_size_result == ZX_OK);
// Create a GPU image using the vmo exported above.
fuchsia::images::ImageInfo fx_image_info{
.width = kImageSize,
.height = kImageSize,
.stride = static_cast<uint32_t>(
kImageSize * images::StrideBytesPerWidthPixel(fuchsia::images::PixelFormat::R8G8B8A8)),
.pixel_format = fuchsia::images::PixelFormat::R8G8B8A8,
.tiling = fuchsia::images::Tiling::GPU_OPTIMAL,
};
// Present the external GPU image using BackgroundView.
view.SetImage(std::move(image_vmo), vmo_size, fx_image_info,
fuchsia::images::MemoryType::VK_DEVICE_MEMORY);
RunUntilIndirectPresent(&view);
scenic::Screenshot screenshot = TakeScreenshot();
ASSERT_FALSE(screenshot.empty());
std::map<scenic::Color, size_t> histogram = screenshot.Histogram();
EXPECT_GT(histogram[kImageColor], 0u);
histogram.erase(kImageColor);
// This assert is written this way so that, when it fails, it prints out all
// the unexpected colors
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram) << "Unexpected colors";
}
// Create the following Scene:
// ----------------------------------
// | View 1 |
// | red |
// |--------------------------------|
// | blue View 2 green |
// | : |
// ----------------------------------
//
// This test case creates three Views: View 1 (containing one red ShapeNode),
// View 2 (containing one blue ShapeNode), Annotation View (containing one green
// ShapeNode).
//
// This test case uses fuchsia.ui.annotation.Registry FIDL API to create
// ViewHolder of Annotation View and attach Annotation View to scene later when
// we call Present() on any Session.
//
// View 2 and Annotation View should have the same View properties.
//
TEST_F(ScenicPixelTest, AnnotationTest) {
auto test_session = SetUpTestSession();
scenic::Session* const session = &test_session->session;
const auto [display_width, display_height] = test_session->display_dimensions;
// Initialize second session
auto unique_session_view1 = std::make_unique<scenic::Session>(scenic());
auto unique_session_view2 = std::make_unique<scenic::Session>(scenic());
auto unique_session_annotation = std::make_unique<scenic::Session>(scenic());
auto session_view1 = unique_session_view1.get();
auto session_view2 = unique_session_view2.get();
auto session_annotation = unique_session_annotation.get();
session_view1->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Session terminated.";
QuitLoop();
});
session_view2->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Session terminated.";
QuitLoop();
});
session_annotation->set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Annotation Session terminated.";
QuitLoop();
});
test_session->SetUpCamera().SetProjection(0);
scenic::EntityNode entity_node(session);
entity_node.SetTranslation(0, 0, 0);
test_session->scene.AddChild(entity_node);
// Create two sets of view/view-holder token pairs.
auto [view_token_1, view_holder_token_1] = scenic::ViewTokenPair::New();
auto [view_token_2, view_holder_token_2] = scenic::ViewTokenPair::New();
auto [view_control_ref_2, view_ref_2] = scenic::ViewRefPair::New();
auto [view_token_annotation, view_holder_token_annotation] = scenic::ViewTokenPair::New();
fuchsia::ui::views::ViewRef view_ref_2_create;
view_ref_2.Clone(&view_ref_2_create);
scenic::View view1(session_view1, std::move(view_token_1), "View 1");
scenic::View view2(session_view2, std::move(view_token_2), std::move(view_control_ref_2),
std::move(view_ref_2_create), "View 2");
scenic::View view_annotation(session_annotation, std::move(view_token_annotation),
"View Annotation");
scenic::ViewHolder view_holder1(session, std::move(view_holder_token_1), "ViewHolder 1");
scenic::ViewHolder view_holder2(session, std::move(view_holder_token_2), "ViewHolder 2");
// Bounds of each view should be the size of a quarter of the display with
// origin at 0,0 relative to its transform node.
const std::array<float, 3> bmin = {0.f, 0.f, -2.f};
const std::array<float, 3> bmax = {display_width, display_height / 2, 1.f};
const std::array<float, 3> imin = {0, 0, 0};
const std::array<float, 3> imax = {0, 0, 0};
view_holder1.SetViewProperties(bmin, bmax, imin, imax);
view_holder2.SetViewProperties(bmin, bmax, imin, imax);
view_holder2.SetTranslation(0, display_height / 2, 0);
// Pane extends across the entire right-side of the display, even though
// its containing view is only in the top-right corner.
int32_t pane_width = display_width;
int32_t pane_height = display_height / 2;
FX_LOGS(ERROR) << pane_width << " " << pane_height;
scenic::Rectangle pane_shape(session_view1, pane_width, pane_height);
scenic::Rectangle pane_shape2(session_view2, pane_width / 2, pane_height);
scenic::Rectangle pane_shape_annotation(session_annotation, pane_width / 2, pane_height);
// Create pane materials.
scenic::Material pane_material_view1(session_view1);
scenic::Material pane_material_view2(session_view2);
scenic::Material pane_material_annotation(session_annotation);
pane_material_view1.SetColor(255, 0, 0, 255); // Red
pane_material_view2.SetColor(0, 0, 255, 255); // Blue
pane_material_annotation.SetColor(0, 255, 0, 255); // Green
scenic::ShapeNode pane_node(session_view1);
pane_node.SetShape(pane_shape);
pane_node.SetMaterial(pane_material_view1);
pane_node.SetTranslation(pane_width / 2, pane_height / 2, 0);
scenic::ShapeNode pane_node2(session_view2);
pane_node2.SetShape(pane_shape2);
pane_node2.SetMaterial(pane_material_view2);
pane_node2.SetTranslation(pane_width / 4, pane_height / 2, 0);
scenic::ShapeNode pane_node_annotation(session_annotation);
pane_node_annotation.SetShape(pane_shape_annotation);
pane_node_annotation.SetMaterial(pane_material_annotation);
pane_node_annotation.SetTranslation(pane_width * 3 / 4, pane_height / 2, 0);
// Add view holders to the transform.
entity_node.AddChild(view_holder1);
view1.AddChild(pane_node);
entity_node.AddChild(view_holder2);
view2.AddChild(pane_node2);
view_annotation.AddChild(pane_node_annotation);
Present(session);
Present(session_view1);
Present(session_view2);
Present(session_annotation);
bool view_holder_annotation_created = false;
fuchsia::ui::views::ViewRef view_ref_2_annotation;
view_ref_2.Clone(&view_ref_2_annotation);
annotation_registry()->CreateAnnotationViewHolder(
std::move(view_ref_2_annotation), std::move(view_holder_token_annotation),
[&view_holder_annotation_created]() { view_holder_annotation_created = true; });
RunLoopWithTimeout(zx::msec(100));
EXPECT_FALSE(view_holder_annotation_created);
{
scenic::Screenshot screenshot = TakeScreenshot();
scenic::Color red_color = screenshot.ColorAt(0.5, 0.25);
scenic::Color blue_color = screenshot.ColorAt(0.25, 0.75);
scenic::Color black_color = screenshot.ColorAt(0.75, 0.75);
EXPECT_EQ(red_color, scenic::Color(255, 0, 0, 255));
EXPECT_EQ(blue_color, scenic::Color(0, 0, 255, 255));
EXPECT_EQ(black_color, scenic::Color(0, 0, 0, 0));
}
Present(session_view2);
EXPECT_TRUE(view_holder_annotation_created);
{
scenic::Screenshot screenshot = TakeScreenshot();
scenic::Color red_color = screenshot.ColorAt(0.5, 0.25);
scenic::Color blue_color = screenshot.ColorAt(0.25, 0.75);
scenic::Color green_color = screenshot.ColorAt(0.75, 0.75);
EXPECT_EQ(red_color, scenic::Color(255, 0, 0, 255));
EXPECT_EQ(blue_color, scenic::Color(0, 0, 255, 255));
EXPECT_EQ(green_color, scenic::Color(0, 255, 0, 255));
}
}
} // namespace