chromium/ui/ozone/platform/flatland/flatland_surface_canvas_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ui/ozone/platform/flatland/flatland_surface_canvas.h"

#include <fuchsia/sysmem/cpp/fidl.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/ui/scenic/cpp/testing/fake_flatland.h>

#include "base/fuchsia/koid.h"
#include "base/fuchsia/process_context.h"
#include "base/fuchsia/scoped_service_publisher.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/test/fidl_matchers.h"
#include "base/test/task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "ui/ozone/platform/flatland/flatland_surface_factory.h"

using ::scenic::FakeGraph;
using ::scenic::FakeImage;
using ::scenic::FakeTransform;
using ::scenic::FakeTransformPtr;
using ::scenic::FakeView;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::Optional;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::VariantWith;

namespace ui {

namespace {

const size_t kImageSize = 100;

Matcher<fuchsia::ui::composition::ImageProperties> IsImageProperties(
    const fuchsia::math::SizeU& size) {
  return AllOf(
      Property("has_size", &fuchsia::ui::composition::ImageProperties::has_size,
               true),
      Property("size", &fuchsia::ui::composition::ImageProperties::size,
               ::base::test::FidlEq(size)));
}

Matcher<FakeGraph> IsSurfaceGraph(
    const fuchsia::ui::views::ViewportCreationToken& viewport_creation_token,
    const fuchsia::math::SizeU& size,
    fuchsia::ui::composition::BlendMode blend_mode,
    const fuchsia::math::Vec& translation = FakeTransform::kDefaultTranslation,
    const fuchsia::math::SizeU& destination_size =
        FakeImage::kDefaultDestinationSize,
    float image_opacity = FakeImage::kDefaultOpacity) {
  auto view_token_koid = base::GetRelatedKoid(viewport_creation_token.value);

  return AllOf(
      Field(
          "root_transform", &FakeGraph::root_transform,
          Pointee(AllOf(
              Field("translation", &FakeTransform::translation,
                    ::base::test::FidlEq(FakeTransform::kDefaultTranslation)),
              Field("scale", &FakeTransform::scale,
                    ::base::test::FidlEq(FakeTransform::kDefaultScale)),
              Field("opacity", &FakeTransform::opacity,
                    FakeTransform::kDefaultOpacity),
              Field("children", &FakeTransform::children, IsEmpty()),
              Field("content", &FakeTransform::content,
                    Pointee(VariantWith<FakeImage>(AllOf(
                        Field("image_properties", &FakeImage::image_properties,
                              IsImageProperties(size)),
                        Field("destination_size", &FakeImage::destination_size,
                              ::base::test::FidlEq(destination_size)),
                        Field("blend_mode", &FakeImage::blend_mode, blend_mode),
                        Field("opacity", &FakeImage::opacity, image_opacity)))))

                  ))),
      Field("view", &FakeGraph::view,
            Optional(
                Field("view_token", &FakeView::view_token, view_token_koid))));
}

fuchsia::sysmem2::AllocatorSyncPtr ConnectSysmemAllocator() {
  fuchsia::sysmem2::AllocatorSyncPtr allocator;
  base::ComponentContextForProcess()->svc()->Connect(allocator.NewRequest());
  return allocator;
}

size_t RoundUp(size_t value, size_t alignment) {
  return ((value + alignment - 1) / alignment) * alignment;
}

}  // namespace

class FlatlandSurfaceCanvasTest : public ::testing::Test {
 public:
  FlatlandSurfaceCanvasTest()
      : sysmem_allocator_(ConnectSysmemAllocator()),
        fake_flatland_publisher_(test_context_.additional_services(),
                                 fake_flatland_.GetFlatlandRequestHandler()) {}
  ~FlatlandSurfaceCanvasTest() override {}

 protected:
  struct FrameBuffer {
    base::ReadOnlySharedMemoryMapping mapping;
  };

  void InitializeCanvas() {
    canvas_ = std::make_unique<FlatlandSurfaceCanvas>(sysmem_allocator_.get(),
                                                      &fake_flatland_);
    auto platform_handle = canvas_->CreateView();
    viewport_creation_token_ = {.value =
                                    zx::channel(platform_handle.TakeHandle())};
    task_environment_.RunUntilIdle();

    canvas_->ResizeCanvas(gfx::Size(kImageSize, kImageSize), 1.0);

    task_environment_.RunUntilIdle();
    ASSERT_EQ(fake_flatland_.graph_bindings().buffer_collections.size(), 1U);

    fuchsia::sysmem2::BufferCollectionSyncPtr buffer_collection;
    sysmem_allocator_->BindSharedCollection(std::move(
        fuchsia::sysmem2::AllocatorBindSharedCollectionRequest{}
            .set_token(fuchsia::sysmem2::BufferCollectionTokenHandle(
                fake_flatland_.graph_bindings()
                    .buffer_collections.begin()
                    ->second.sysmem_token.TakeChannel()))
            .set_buffer_collection_request(buffer_collection.NewRequest())));

    fuchsia::sysmem2::BufferCollectionConstraints constraints;
    constraints.mutable_usage()->set_cpu(fuchsia::sysmem2::CPU_USAGE_READ);

    auto& memory_constraints = *constraints.mutable_buffer_memory_constraints();
    memory_constraints.set_cpu_domain_supported(true);

    buffer_collection->SetConstraints(
        std::move(fuchsia::sysmem2::BufferCollectionSetConstraintsRequest{}
                      .set_constraints(std::move(constraints))));

    fuchsia::sysmem2::BufferCollection_WaitForAllBuffersAllocated_Result
        wait_result;
    zx_status_t status =
        buffer_collection->WaitForAllBuffersAllocated(&wait_result);
    ASSERT_EQ(status, ZX_OK);
    ASSERT_TRUE(wait_result.is_response());
    auto buffer_info =
        std::move(*wait_result.response().mutable_buffer_collection_info());

    buffer_collection->Release();

    // sysmem always fills out settings(), image_format_constraints(), min_size().
    EXPECT_GE(
        buffer_info.settings().image_format_constraints().min_size().width,
        kImageSize);
    EXPECT_GE(
        buffer_info.settings().image_format_constraints().min_size().height,
        kImageSize);

    // sysmem always fills out buffers()
    for (size_t i = 0; i < buffer_info.buffers().size(); ++i) {
      auto memory_region = base::ReadOnlySharedMemoryRegion::Deserialize(
          base::subtle::PlatformSharedMemoryRegion::Take(
              std::move(*buffer_info.mutable_buffers()->at(i).mutable_vmo()),
              base::subtle::PlatformSharedMemoryRegion::Mode::kReadOnly,
              buffer_info.settings().buffer_settings().size_bytes(),
              base::UnguessableToken::Create()));
      auto mapping = memory_region.Map();
      ASSERT_TRUE(mapping.IsValid());
      frames_.push_back(FrameBuffer{.mapping = std::move(mapping)});
    }

    const fuchsia::sysmem2::ImageFormatConstraints& format =
        buffer_info.settings().image_format_constraints();
    image_stride_ =
        RoundUp(std::max(static_cast<size_t>(format.min_bytes_per_row()),
                         kImageSize * 4U),
                format.bytes_per_row_divisor());
  }

  void PresentFrame(SkRect dirty_rect = SkRect()) {
    if (dirty_rect.isEmpty()) {
      dirty_rect = SkRect::MakeWH(kImageSize, kImageSize);
    }

    canvas_->PresentCanvas(gfx::Rect(dirty_rect.left(), dirty_rect.top(),
                                     dirty_rect.width(), dirty_rect.height()));
    task_environment_.RunUntilIdle();

    fuchsia::ui::composition::OnNextFrameBeginValues begin_values;
    begin_values.set_additional_present_credits(1);
    fake_flatland_.FireOnNextFrameBeginEvent(std::move(begin_values));
    task_environment_.RunUntilIdle();

    ASSERT_TRUE(std::holds_alternative<FakeImage>(
        *fake_flatland_.graph().root_transform->content));
    ASSERT_EQ(
        std::get<FakeImage>(*fake_flatland_.graph().root_transform->content)
            .collection_id,
        fake_flatland_.graph_bindings().buffer_collections.begin()->first);
  }

  SkColor GetPixelValueAt(size_t x, size_t y) {
    int frame_index =
        std::get<FakeImage>(*fake_flatland_.graph().root_transform->content)
            .vmo_index;
    const char* image_data =
        reinterpret_cast<const char*>(frames_[frame_index].mapping.memory());
    size_t pixel_offset = y * image_stride_ + x * sizeof(SkColor);
    auto result = *reinterpret_cast<const SkColor*>(image_data + pixel_offset);
    return result;
  }

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::MainThreadType::IO};

  fuchsia::sysmem2::AllocatorSyncPtr sysmem_allocator_;
  scenic::FakeFlatland fake_flatland_;

  base::TestComponentContextForProcess test_context_;

  // Injects binding for responding to Flatland protocol connection requests.
  const base::ScopedServicePublisher<fuchsia::ui::composition::Flatland>
      fake_flatland_publisher_;

  std::unique_ptr<FlatlandSurfaceCanvas> canvas_ = nullptr;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token_;

  std::vector<FrameBuffer> frames_;
  size_t image_stride_ = 0;
};

TEST_F(FlatlandSurfaceCanvasTest, InitializeAndPresent) {
  ASSERT_NO_FATAL_FAILURE(InitializeCanvas());

  auto* canvas = canvas_->GetCanvas();
  EXPECT_TRUE(canvas);

  canvas_->PresentCanvas(gfx::Rect(kImageSize, kImageSize));
  task_environment_.RunUntilIdle();

  EXPECT_THAT(fake_flatland_.graph(),
              IsSurfaceGraph(viewport_creation_token_, {kImageSize, kImageSize},
                             fuchsia::ui::composition::BlendMode::SRC_OVER));
}

TEST_F(FlatlandSurfaceCanvasTest, ImageContent) {
  ASSERT_NO_FATAL_FAILURE(InitializeCanvas());

  // First, present a blue frame.
  auto* canvas = canvas_->GetCanvas();
  ASSERT_TRUE(canvas);
  canvas->drawColor(SK_ColorBLUE);

  ASSERT_NO_FATAL_FAILURE(PresentFrame());

  EXPECT_EQ(GetPixelValueAt(0, 0), SK_ColorBLUE);
  EXPECT_EQ(GetPixelValueAt(kImageSize / 2, kImageSize / 2), SK_ColorBLUE);

  // Fill the bottom right corner to green.
  canvas = canvas_->GetCanvas();
  SkRect green_rect = SkRect::MakeXYWH(kImageSize / 2, kImageSize / 2,
                                       kImageSize / 2, kImageSize / 2);

  ASSERT_TRUE(canvas);
  canvas->clipRect(green_rect);
  canvas->drawColor(SK_ColorGREEN);

  ASSERT_NO_FATAL_FAILURE(PresentFrame(green_rect));

  EXPECT_EQ(GetPixelValueAt(0, 0), SK_ColorBLUE);
  EXPECT_EQ(GetPixelValueAt(kImageSize / 2, kImageSize / 2), SK_ColorGREEN);
}

}  // namespace ui