[scenic] Add simple pixel tests
Adds two tests:
* Renders a solid color. All pixels are asserted to be this color.
* Renders a static square. Screenshot is saved to a file for consumption by
Skia Gold (integration to come later; this does not yet do a diff).
This test only executes on hardware (see changes to bin/ui/BUILD.gn).
Test: fx run-test scenic_tests -t gfx_pixeltests
Replaced solid color with square to verify failure case.
Change-Id: I58b8f77d14c60be9a69cfc1141cd2e0e4603a727
diff --git a/bin/ui/BUILD.gn b/bin/ui/BUILD.gn
index 4a6f9f4..3108c31 100644
--- a/bin/ui/BUILD.gn
+++ b/bin/ui/BUILD.gn
@@ -31,6 +31,16 @@
name = "gfx_apptests"
},
{
+ name = "gfx_pixeltests"
+ environments = [
+ {
+ dimensions = {
+ device_type = "Intel NUC Kit NUC7i5DNHE"
+ }
+ },
+ ]
+ },
+ {
name = "gfx_unittests"
},
{
@@ -66,6 +76,10 @@
dest = "gfx_apptests.cmx"
},
{
+ path = rebase_path("meta/gfx_pixeltests.cmx")
+ dest = "gfx_pixeltests.cmx"
+ },
+ {
path = rebase_path("meta/gfx_unittests.cmx")
dest = "gfx_unittests.cmx"
},
@@ -229,7 +243,6 @@
}
package("scenic_tools") {
-
deps = [
"gltf_export",
"input",
diff --git a/bin/ui/meta/gfx_pixeltests.cmx b/bin/ui/meta/gfx_pixeltests.cmx
new file mode 100644
index 0000000..f7eaf61
--- /dev/null
+++ b/bin/ui/meta/gfx_pixeltests.cmx
@@ -0,0 +1,14 @@
+{
+ "program": {
+ "binary": "test/gfx_pixeltests"
+ },
+ "sandbox": {
+ "features": [
+ "system-temp"
+ ],
+ "services": [
+ "fuchsia.sys.Environment",
+ "fuchsia.sys.Loader"
+ ]
+ }
+}
diff --git a/lib/ui/gfx/tests/BUILD.gn b/lib/ui/gfx/tests/BUILD.gn
index 1f4a79f..e4cfc71 100644
--- a/lib/ui/gfx/tests/BUILD.gn
+++ b/lib/ui/gfx/tests/BUILD.gn
@@ -8,6 +8,7 @@
testonly = true
public_deps = [
":apptests",
+ ":pixeltests",
":unittests",
]
}
@@ -105,6 +106,32 @@
]
}
+executable("pixeltests") {
+ output_name = "gfx_pixeltests"
+
+ testonly = true
+ sources = [
+ "scenic_pixel_test.cc",
+ ]
+ include_dirs = [
+ "//garnet/public/lib/escher",
+ "//third_party/glm",
+ ]
+ deps = [
+ "//garnet/public/fidl/fuchsia.sys",
+ "//garnet/public/fidl/fuchsia.ui.policy",
+ "//garnet/public/fidl/fuchsia.ui.scenic",
+ "//garnet/public/lib/component/cpp/testing",
+ "//garnet/public/lib/fsl",
+ "//garnet/public/lib/fxl",
+ "//garnet/public/lib/gtest",
+ "//garnet/public/lib/ui/gfx/cpp",
+ "//garnet/public/lib/ui/scenic/cpp",
+ "//third_party/googletest:gtest_main",
+ "//zircon/public/lib/async-cpp",
+ ]
+}
+
executable("mock_pose_buffer_provider_cc") {
output_name = "mock_pose_buffer_provider"
diff --git a/lib/ui/gfx/tests/scenic_pixel_test.cc b/lib/ui/gfx/tests/scenic_pixel_test.cc
new file mode 100644
index 0000000..1436762
--- /dev/null
+++ b/lib/ui/gfx/tests/scenic_pixel_test.cc
@@ -0,0 +1,348 @@
+// 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 <fstream>
+#include <map>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <fuchsia/ui/policy/cpp/fidl.h>
+#include <fuchsia/ui/scenic/cpp/fidl.h>
+
+#include <gtest/gtest.h>
+#include <lib/async/cpp/task.h>
+#include <lib/component/cpp/testing/test_with_environment.h>
+#include <lib/fsl/vmo/vector.h>
+#include <lib/fxl/logging.h>
+#include <lib/fxl/strings/string_printf.h>
+#include <lib/ui/gfx/cpp/math.h>
+#include <lib/ui/scenic/cpp/resources.h>
+#include <lib/ui/scenic/cpp/session.h>
+
+namespace {
+
+constexpr char kEnvironment[] = "ScenicPixelTest";
+
+// 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.Presenter2",
+ "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"}};
+
+struct ViewContext {
+ scenic::SessionPtrAndListenerRequest session_and_listener_request;
+ zx::eventpair view_token;
+};
+
+// Represents a view that allows a callback to be set for its |Present|.
+class TestView {
+ public:
+ virtual ~TestView() = default;
+ virtual void set_present_callback(
+ scenic::Session::PresentCallback present_callback) = 0;
+};
+
+// 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 component::testing::TestWithEnvironment {
+ protected:
+ ScenicPixelTest() {
+ std::unique_ptr<component::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: " << 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();
+ });
+ RunLoop();
+ return screenshot_out;
+ }
+
+ // Dumps the screenshot to a shared temporary file and returns the filename.
+ std::string DumpScreenshot(fuchsia::ui::scenic::ScreenshotData screenshot) {
+ std::vector<uint8_t> data;
+ EXPECT_TRUE(fsl::VectorFromVmo(screenshot.data, &data))
+ << "Failed to read screenshot";
+ const auto* test_info =
+ testing::UnitTest::GetInstance()->current_test_info();
+ std::ostringstream filename;
+ filename << "/tmp/screenshot-" << test_info->test_case_name()
+ << "::" << test_info->name() << "-" << screenshot_index_;
+
+ std::ofstream fout(filename.str());
+ EXPECT_TRUE(fout.good())
+ << "Failed to open " << filename.str() << " for writing";
+
+ // Convert to PPM. We'll probably need PNG instead for Skia Gold.
+ fout << "P6\n"
+ << screenshot.info.width << "\n"
+ << screenshot.info.height << "\n"
+ << 255 << "\n";
+
+ const uint8_t* pchannel = data.data();
+ for (uint32_t pixel = 0;
+ pixel < screenshot.info.width * screenshot.info.height; pixel++) {
+ uint8_t rgb[] = {pchannel[2], pchannel[1], pchannel[0]};
+ fout.write(reinterpret_cast<const char*>(rgb), 3);
+ pchannel += 4;
+ }
+
+ fout.flush();
+
+ EXPECT_TRUE(fout.good())
+ << "Failed to write screenshot to " << filename.str();
+
+ ++screenshot_index_;
+ return filename.str();
+ }
+
+ // Create a |ViewContext| that allows us to present a view via
+ // |RootPresenter|. See also examples/ui/hello_base_view
+ ViewContext CreatePresentationContext() {
+ zx::eventpair view_holder_token, view_token;
+ FXL_CHECK(zx::eventpair::create(0u, &view_holder_token, &view_token) ==
+ ZX_OK);
+
+ ViewContext view_context = {
+ .session_and_listener_request =
+ scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()),
+ .view_token = std::move(view_token),
+ };
+
+ fuchsia::ui::policy::Presenter2Ptr 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(TestView* view) {
+ view->set_present_callback([this](fuchsia::images::PresentationInfo info) {
+ async::PostTaskForTime(dispatcher(), QuitLoopClosure(),
+ static_cast<zx::time>(info.presentation_time));
+ });
+
+ RunLoop();
+ }
+
+ fuchsia::ui::scenic::ScenicPtr scenic_;
+
+ private:
+ std::unique_ptr<component::testing::EnclosingEnvironment> environment_;
+ uint screenshot_index_ = 0;
+};
+
+struct Color {
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+ uint8_t a;
+};
+
+inline bool operator==(const Color& a, const Color& b) {
+ return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
+}
+
+inline bool operator<(const Color& a, const Color& b) {
+ return std::tie(a.r, a.g, a.b, a.a) < std::tie(b.r, b.g, b.b, b.a);
+}
+
+// RGBA hex dump
+std::ostream& operator<<(std::ostream& os, const Color& c) {
+ return os << fxl::StringPrintf("%02X%02X%02X%02X", c.r, c.g, c.b, c.a);
+}
+
+// Base view with a solid background.
+// See also lib/ui/base_view
+class BackgroundView : public TestView,
+ private fuchsia::ui::scenic::SessionListener {
+ public:
+ static constexpr float kBackgroundElevation = 0.f;
+ static constexpr Color kBackgroundColor = {0x67, 0x3a, 0xb7,
+ 0xff}; // Deep Purple 500
+
+ BackgroundView(ViewContext context,
+ const std::string& debug_name = "BackgroundView")
+ : binding_(this, std::move(context.session_and_listener_request.second)),
+ session_(std::move(context.session_and_listener_request.first)),
+ view_(&session_, std::move(context.view_token), debug_name),
+ background_node_(&session_) {
+ binding_.set_error_handler([](zx_status_t status) { FAIL() << status; });
+ session_.Present(0, [](auto) {});
+
+ scenic::Material background_material(&session_);
+ background_material.SetColor(kBackgroundColor.r, kBackgroundColor.g,
+ kBackgroundColor.b, kBackgroundColor.a);
+ background_node_.SetMaterial(background_material);
+ view_.AddChild(background_node_);
+ }
+
+ void set_present_callback(
+ scenic::Session::PresentCallback present_callback) override {
+ present_callback_ = std::move(present_callback);
+ }
+
+ protected:
+ scenic::Session* session() { return &session_; }
+
+ scenic::View* view() { return &view_; }
+
+ virtual void Draw(float cx, float cy, float sx, float sy) {
+ scenic::Rectangle background_shape(&session_, sx, sy);
+ background_node_.SetShape(background_shape);
+ background_node_.SetTranslation((float[]){cx, cy, kBackgroundElevation});
+ }
+
+ private:
+ void OnScenicEvent(std::vector<fuchsia::ui::scenic::Event> events) override {
+ for (const auto& event : events) {
+ if (event.Which() == fuchsia::ui::scenic::Event::Tag::kGfx &&
+ event.gfx().Which() ==
+ fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged) {
+ const auto& evt = event.gfx().view_properties_changed();
+ fuchsia::ui::gfx::BoundingBox layout_box =
+ scenic::ViewPropertiesLayoutBox(evt.properties);
+
+ const auto sz = scenic::Max(layout_box.max - layout_box.min, 0.f);
+ OnViewPropertiesChanged(sz);
+ }
+ }
+ }
+
+ void OnScenicError(std::string error) override { FAIL() << error; }
+
+ void OnViewPropertiesChanged(const fuchsia::ui::gfx::vec3& sz) {
+ if (!sz.x || !sz.y || !sz.z)
+ return;
+
+ Draw(sz.x * .5f, sz.y * .5f, sz.x, sz.y);
+
+ session_.Present(0, std::move(present_callback_));
+ }
+
+ fidl::Binding<fuchsia::ui::scenic::SessionListener> binding_;
+ scenic::Session session_;
+ scenic::View view_;
+
+ scenic::ShapeNode background_node_;
+ scenic::Session::PresentCallback present_callback_;
+};
+
+// Displays a static frame of the Spinning Square example.
+// See also examples/ui/spinning_square
+class RotatedSquareView : public BackgroundView {
+ public:
+ static constexpr float kSquareElevation = 8.f;
+ static constexpr float kAngle = M_PI / 4;
+
+ RotatedSquareView(ViewContext context,
+ const std::string& debug_name = "RotatedSquareView")
+ : BackgroundView(std::move(context), debug_name),
+ square_node_(session()) {
+ scenic::Material square_material(session());
+ square_material.SetColor(0xf5, 0x00, 0x57, 0xff); // Pink A400
+ square_node_.SetMaterial(square_material);
+ view()->AddChild(square_node_);
+ }
+
+ private:
+ void Draw(float cx, float cy, float sx, float sy) override {
+ BackgroundView::Draw(cx, cy, sx, sy);
+
+ const float square_size = std::min(sx, sy) * .6f;
+
+ scenic::Rectangle square_shape(session(), square_size, square_size);
+ square_node_.SetShape(square_shape);
+ square_node_.SetTranslation((float[]){cx, cy, kSquareElevation});
+ square_node_.SetRotation(
+ (float[]){0.f, 0.f, sinf(kAngle * .5f), cosf(kAngle * .5f)});
+ }
+
+ scenic::ShapeNode square_node_;
+};
+
+TEST_F(ScenicPixelTest, SolidColor) {
+ BackgroundView 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";
+
+ 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<Color, size_t> histogram;
+
+ // https://en.wikipedia.org/wiki/Sword_Art_Online_Alternative_Gun_Gale_Online#Characters
+ const uint8_t* pchan = data.data();
+ const size_t llenn = screenshot.info.width * screenshot.info.height;
+ for (uint32_t pixel = 0; pixel < llenn; pixel++) {
+ Color color = {pchan[2], pchan[1], pchan[0], pchan[3]};
+ ++histogram[color];
+ pchan += 4;
+ }
+
+ EXPECT_GT(histogram[BackgroundView::kBackgroundColor], 0u);
+ histogram.erase(BackgroundView::kBackgroundColor);
+ EXPECT_EQ((std::map<Color, size_t>){}, histogram) << "Unexpected colors";
+}
+
+// Renders a rotated square to the screen and saves a screenshot for later
+// screendiff validation.
+TEST_F(ScenicPixelTest, RotatedSquare) {
+ RotatedSquareView view(CreatePresentationContext());
+ RunUntilPresent(&view);
+
+ const std::string filename = DumpScreenshot(TakeScreenshot());
+ FXL_LOG(INFO) << "Wrote screenshot to " << filename;
+}
+
+} // namespace
\ No newline at end of file