blob: f176bcb858ed17ed6ffb4457b13c9fe538e5c327 [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.
#include <map>
#include <string>
#include <vector>
#include <fuchsia/ui/gfx/cpp/fidl.h>
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <gtest/gtest.h>
#include <lib/async/cpp/task.h>
#include <lib/fsl/vmo/vector.h>
#include <lib/fxl/logging.h>
#include <lib/sys/cpp/testing/test_with_environment.h>
#include <lib/ui/scenic/cpp/session.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <zircon/status.h>
#include "garnet/testing/views/background_view.h"
#include "garnet/testing/views/coordinate_test_view.h"
#include "garnet/testing/views/test_view.h"
namespace {
constexpr char kEnvironment[] = "ScenicPixelTest";
constexpr zx::duration kTimeout = zx::sec(15);
// These tests need Scenic and RootPresenter at minimum, which expand to the
// dependencies below. Using |TestWithEnvironment|, we use
// |fuchsia.sys.Environment| and |fuchsia.sys.Loader| from the system (declared
// in our *.cmx sandbox) and launch these other services in the environment we
// create in our test fixture.
//
// Another way to do this would be to whitelist these services in our sandbox
// and inject/start them via the |fuchsia.test| facet. However that has the
// disadvantage that it uses one instance of those services across all tests in
// the binary, making each test not hermetic wrt. the others. A trade-off is
// that the |TestWithEnvironment| method is more verbose.
const std::map<std::string, std::string> kServices = {
{"fuchsia.tracelink.Registry",
"fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx"},
{"fuchsia.ui.policy.Presenter",
"fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx"},
{"fuchsia.ui.scenic.Scenic",
"fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx"},
{"fuchsia.vulkan.loader.Loader",
"fuchsia-pkg://fuchsia.com/vulkan_loader#meta/vulkan_loader.cmx"},
{"fuchsia.sysmem.Allocator",
"fuchsia-pkg://fuchsia.com/sysmem_connector#meta/sysmem_connector.cmx"}};
// Test fixture that sets up an environment suitable for Scenic pixel tests
// and provides related utilities. The environment includes Scenic and
// RootPresenter, and their dependencies.
class ScenicPixelTest : public sys::testing::TestWithEnvironment {
protected:
ScenicPixelTest() {
std::unique_ptr<sys::testing::EnvironmentServices> services =
CreateServices();
for (const auto& entry : kServices) {
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = entry.second;
services->AddServiceWithLaunchInfo(std::move(launch_info), entry.first);
}
environment_ =
CreateNewEnclosingEnvironment(kEnvironment, std::move(services));
environment_->ConnectToService(scenic_.NewRequest());
scenic_.set_error_handler([this](zx_status_t status) {
FAIL() << "Lost connection to Scenic: " << zx_status_get_string(status);
});
}
// Blocking wrapper around |Scenic::TakeScreenshot|. This should not be called
// from within a loop |Run|, as it spins up its own to block and nested loops
// are undefined behavior.
fuchsia::ui::scenic::ScreenshotData TakeScreenshot() {
fuchsia::ui::scenic::ScreenshotData screenshot_out;
scenic_->TakeScreenshot(
[this, &screenshot_out](fuchsia::ui::scenic::ScreenshotData screenshot,
bool status) {
EXPECT_TRUE(status) << "Failed to take screenshot";
screenshot_out = std::move(screenshot);
QuitLoop();
});
EXPECT_FALSE(RunLoopWithTimeout(kTimeout))
<< "Timed out waiting for screenshot.";
return screenshot_out;
}
// Create a |ViewContext| that allows us to present a view via
// |RootPresenter|. See also examples/ui/hello_base_view
scenic::ViewContext CreatePresentationContext() {
auto [view_token, view_holder_token] = scenic::NewViewTokenPair();
scenic::ViewContext view_context = {
.session_and_listener_request =
scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()),
.view_token2 = std::move(view_token),
};
fuchsia::ui::policy::PresenterPtr presenter;
environment_->ConnectToService(presenter.NewRequest());
presenter->PresentView(std::move(view_holder_token), nullptr);
return view_context;
}
// Runs until the view renders its next frame. Technically, waits until the
// |Present| callback is invoked with an expected presentation timestamp, and
// then waits until that time.
void RunUntilPresent(scenic::TestView* view) {
// Typical sequence of events:
// 1. We set up a view bound as a |SessionListener|.
// 2. The view sends its initial |Present| to get itself connected, without
// a callback.
// 3. We call |RunUntilPresent| which sets a present callback on our
// |TestView|.
// 4. |RunUntilPresent| runs the message loop, which allows the view to
// receive a Scenic event telling us our metrics.
// 5. In response, the view sets up the scene graph with the test scene.
// 6. The view calls |Present| with the callback set in |RunUntilPresent|.
// 7. The still-running message loop eventually dispatches the present
// callback.
// 8. The callback schedules a quit for the presentation timestamp we got.
// 9. The message loop eventually dispatches the quit and exits.
view->set_present_callback([this](fuchsia::images::PresentationInfo info) {
zx::time presentation_time =
static_cast<zx::time>(info.presentation_time);
FXL_LOG(INFO)
<< "Present scheduled for "
<< (presentation_time - zx::clock::get_monotonic()).to_msecs()
<< " ms from now";
async::PostTaskForTime(dispatcher(), QuitLoopClosure(),
presentation_time);
});
EXPECT_FALSE(RunLoopWithTimeout(kTimeout))
<< "Timed out waiting for present. See surrounding logs for details.";
}
fuchsia::ui::scenic::ScenicPtr scenic_;
private:
std::unique_ptr<sys::testing::EnclosingEnvironment> environment_;
};
TEST_F(ScenicPixelTest, SolidColor) {
scenic::BackgroundView view(CreatePresentationContext());
RunUntilPresent(&view);
fuchsia::ui::scenic::ScreenshotData screenshot = TakeScreenshot();
EXPECT_GT(screenshot.info.width, 0u);
EXPECT_GT(screenshot.info.height, 0u);
// We could assert on each pixel individually, but a histogram might give us a
// more meaningful failure.
std::map<scenic::Color, size_t> histogram = scenic::Histogram(screenshot);
EXPECT_GT(histogram[scenic::BackgroundView::kBackgroundColor], 0u);
histogram.erase(scenic::BackgroundView::kBackgroundColor);
EXPECT_EQ((std::map<scenic::Color, size_t>){}, histogram)
<< "Unexpected colors";
}
TEST_F(ScenicPixelTest, ViewCoordinates) {
// Synchronously get display dimensions
float display_width;
float display_height;
scenic_->GetDisplayInfo([this, &display_width, &display_height](
fuchsia::ui::gfx::DisplayInfo display_info) {
display_width = static_cast<float>(display_info.width_in_px);
display_height = static_cast<float>(display_info.height_in_px);
QuitLoop();
});
RunLoop();
scenic::CoordinateTestView view(CreatePresentationContext());
RunUntilPresent(&view);
fuchsia::ui::scenic::ScreenshotData screenshot = TakeScreenshot();
std::vector<uint8_t> data;
EXPECT_TRUE(fsl::VectorFromVmo(screenshot.data, &data))
<< "Failed to read screenshot";
auto get_color_at_coordinates = [&display_width, &display_height, &data](
float x, float y) -> scenic::Color {
auto pixels = reinterpret_cast<scenic::Color*>(data.data());
uint32_t index_x = x * display_width;
uint32_t index_y = y * display_height;
uint32_t index = index_y * display_width + index_x;
return pixels[index];
};
EXPECT_EQ(scenic::Color({0, 0, 0, 255}),
get_color_at_coordinates(.25f, .25f));
EXPECT_EQ(scenic::Color({0, 0, 255, 255}),
get_color_at_coordinates(.25f, .75f));
EXPECT_EQ(scenic::Color({255, 0, 0, 255}),
get_color_at_coordinates(.75f, .25f));
EXPECT_EQ(scenic::Color({255, 0, 255, 255}),
get_color_at_coordinates(.75f, .75f));
EXPECT_EQ(scenic::Color({0, 255, 0, 255}),
get_color_at_coordinates(.5f, .5f));
}
// Draws and tests the following coordinate test pattern without views:
// ___________________________________
// | | |
// | BLACK | RED |
// | _____|_____ |
// |___________| GREEN |___________|
// | |_________| |
// | | |
// | BLUE | MAGENTA |
// |________________|________________|
//
TEST_F(ScenicPixelTest, GlobalCoordinates) {
// Synchronously get display dimensions
float display_width;
float display_height;
scenic_->GetDisplayInfo([this, &display_width, &display_height](
fuchsia::ui::gfx::DisplayInfo display_info) {
display_width = static_cast<float>(display_info.width_in_px);
display_height = static_cast<float>(display_info.height_in_px);
QuitLoop();
});
RunLoop();
// Initialize session
auto unique_session = std::make_unique<scenic::Session>(scenic_.get());
auto session = unique_session.get();
session->set_error_handler([this](zx_status_t status) {
FXL_LOG(ERROR) << "Session terminated.";
QuitLoop();
});
scenic::DisplayCompositor compositor(session);
scenic::LayerStack layer_stack(session);
scenic::Layer layer(session);
scenic::Renderer renderer(session);
scenic::Scene scene(session);
scenic::Camera camera(scene);
float eye_position[3] = {display_width / 2.f, display_height / 2.f, -1001};
float look_at[3] = {display_width / 2.f, display_height / 2.f, 1};
float up[3] = {0, -1, 0};
camera.SetTransform(eye_position, look_at, up);
compositor.SetLayerStack(layer_stack);
layer_stack.AddLayer(layer);
layer.SetSize(display_width, display_height);
layer.SetRenderer(renderer);
renderer.SetCamera(camera.id());
// Set up lights.
scenic::AmbientLight ambient_light(session);
scene.AddLight(ambient_light);
ambient_light.SetColor(1.f, 1.f, 1.f);
// Create an EntityNode to serve as the scene root.
scenic::EntityNode root_node(session);
scene.AddChild(root_node.id());
static const float pane_width = display_width / 2;
static 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 + 0.5) * pane_width, (j + 0.5) * pane_height,
-20);
root_node.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(0.5 * display_width, 0.5 * display_height, -40);
root_node.AddChild(pane_node);
// Actual tests. Test the same scene with an orthographic and perspective
// camera.
std::string camera_type[2] = {"orthographic", "perspective"};
float fov[2] = {0, 2 * atan((display_height / 2.f) / abs(eye_position[2]))};
for (int i = 0; i < 2; i++) {
FXL_LOG(INFO) << "Testing " << camera_type[i] << " camera";
camera.SetProjection(fov[i]);
session->Present(
0, [this](fuchsia::images::PresentationInfo info) { QuitLoop(); });
RunLoop();
fuchsia::ui::scenic::ScreenshotData screenshot = TakeScreenshot();
std::vector<uint8_t> data;
EXPECT_TRUE(fsl::VectorFromVmo(screenshot.data, &data))
<< "Failed to read screenshot";
auto get_color_at_coordinates = [&display_width, &display_height, &data](
float x, float y) -> scenic::Color {
auto pixels = reinterpret_cast<scenic::Color*>(data.data());
uint32_t index_x = x * display_width;
uint32_t index_y = y * display_height;
uint32_t index = index_y * display_width + index_x;
return pixels[index];
};
EXPECT_EQ(scenic::Color({0, 0, 0, 255}),
get_color_at_coordinates(.25f, .25f));
EXPECT_EQ(scenic::Color({0, 0, 255, 255}),
get_color_at_coordinates(.25f, .75f));
EXPECT_EQ(scenic::Color({255, 0, 0, 255}),
get_color_at_coordinates(.75f, .25f));
EXPECT_EQ(scenic::Color({255, 0, 255, 255}),
get_color_at_coordinates(.75f, .75f));
EXPECT_EQ(scenic::Color({0, 255, 0, 255}),
get_color_at_coordinates(.5f, .5f));
}
}
} // namespace