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

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

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

#include <lib/sys/cpp/component_context.h>
#include <lib/zx/eventpair.h>
#include <zircon/types.h>

#include "base/check_op.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/process_context.h"
#include "base/functional/bind.h"
#include "base/not_fatal_until.h"
#include "base/trace_event/trace_event.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/ozone/platform/flatland/flatland_connection.h"
#include "ui/ozone/platform/flatland/flatland_gpu_host.h"
#include "ui/ozone/platform/flatland/flatland_surface_factory.h"
#include "ui/ozone/platform/flatland/flatland_sysmem_buffer_collection.h"
#include "ui/ozone/platform/flatland/flatland_sysmem_native_pixmap.h"
#include "ui/ozone/public/overlay_plane.h"

namespace ui {

namespace {

std::vector<zx::event> GpuFenceHandlesToZxEvents(
    std::vector<gfx::GpuFenceHandle> handles) {
  std::vector<zx::event> events;
  events.reserve(handles.size());
  for (auto& handle : handles) {
    events.push_back(handle.Release());
  }
  return events;
}

// A struct containing Flatland properties for an associated overlay transform.
// See |OverlayTransformToFlatlandProperties|.
struct OverlayTransformFlatlandProperties {
  fuchsia::math::Vec translation;
  fuchsia::ui::composition::Orientation orientation;
  fuchsia::ui::composition::ImageFlip image_flip;
};

// Converts the overlay transform to the associated Flatland properties. For
// rotation, converts OverlayTransform enum to angle in radians. Since rotation
// occurs around the top-left corner, also returns the associated translation to
// recenter the overlay.
OverlayTransformFlatlandProperties OverlayTransformToFlatlandProperties(
    gfx::OverlayTransform plane_transform,
    gfx::Rect rounded_bounds) {
  switch (plane_transform) {
    case gfx::OVERLAY_TRANSFORM_NONE:
      return {
          .translation = {rounded_bounds.x(), rounded_bounds.y()},
          .orientation = fuchsia::ui::composition::Orientation::CCW_0_DEGREES,
          .image_flip = fuchsia::ui::composition::ImageFlip::NONE};
    // gfx::OverlayTransform and Flatland rotate in opposite directions relative
    // to each other, so swap 90 and 270.
    case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90:
      return {
          .translation = {rounded_bounds.x() + rounded_bounds.width(),
                          rounded_bounds.y()},
          .orientation = fuchsia::ui::composition::Orientation::CCW_270_DEGREES,
          .image_flip = fuchsia::ui::composition::ImageFlip::NONE};
    case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180:
      return {
          .translation = {rounded_bounds.x() + rounded_bounds.width(),
                          rounded_bounds.y() + rounded_bounds.height()},
          .orientation = fuchsia::ui::composition::Orientation::CCW_180_DEGREES,
          .image_flip = fuchsia::ui::composition::ImageFlip::NONE};
    case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270:
      return {
          .translation = {rounded_bounds.x(),
                          rounded_bounds.y() + rounded_bounds.height()},
          .orientation = fuchsia::ui::composition::Orientation::CCW_90_DEGREES,
          .image_flip = fuchsia::ui::composition::ImageFlip::NONE};
    case gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL:
      return {
          .translation = {rounded_bounds.x(), rounded_bounds.y()},
          .orientation = fuchsia::ui::composition::Orientation::CCW_0_DEGREES,
          .image_flip = fuchsia::ui::composition::ImageFlip::LEFT_RIGHT};
    case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL:
      return {
          .translation = {rounded_bounds.x(), rounded_bounds.y()},
          .orientation = fuchsia::ui::composition::Orientation::CCW_0_DEGREES,
          .image_flip = fuchsia::ui::composition::ImageFlip::UP_DOWN,
      };
    case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_90:
    case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL_CLOCKWISE_270:
    case gfx::OVERLAY_TRANSFORM_INVALID:
      break;
  }
  NOTREACHED_IN_MIGRATION();
  return {
      .translation = {rounded_bounds.x(), rounded_bounds.y()},
      .orientation = fuchsia::ui::composition::Orientation::CCW_0_DEGREES,
      .image_flip = fuchsia::ui::composition::ImageFlip::NONE,
  };
}

// Converts a gfx size to the associated Fuchsia size, and accounts for any
// rotation that may be specified in the plane transform (if specified).
fuchsia::math::SizeU GfxSizeToFuchsiaSize(
    const gfx::Size& size,
    gfx::OverlayTransform plane_transform = gfx::OVERLAY_TRANSFORM_NONE) {
  if (plane_transform == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 ||
      plane_transform == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) {
    return fuchsia::math::SizeU{static_cast<uint32_t>(size.height()),
                                static_cast<uint32_t>(size.width())};
  }
  return fuchsia::math::SizeU{static_cast<uint32_t>(size.width()),
                              static_cast<uint32_t>(size.height())};
}

fuchsia::ui::composition::ContentId CreateImage(
    FlatlandConnection* flatland,
    fuchsia::ui::composition::BufferCollectionImportToken import_token,
    const gfx::Size& size,
    uint32_t vmo_index) {
  fuchsia::ui::composition::ImageProperties image_properties;
  image_properties.set_size(GfxSizeToFuchsiaSize(size));
  const fuchsia::ui::composition::ContentId image_id =
      flatland->NextContentId();
  flatland->flatland()->CreateImage(image_id, std::move(import_token),
                                    vmo_index, std::move(image_properties));
  return image_id;
}

}  // namespace

FlatlandSurface::FlatlandSurface(
    FlatlandSurfaceFactory* flatland_surface_factory,
    gfx::AcceleratedWidget window)
    : flatland_("Chromium FlatlandSurface",
                base::BindOnce(&FlatlandSurface::OnFlatlandError,
                               base::Unretained(this))),
      flatland_surface_factory_(flatland_surface_factory),
      window_(window) {
  // Create Flatland Allocator connection.
  flatland_allocator_ = base::ComponentContextForProcess()
                            ->svc()
                            ->Connect<fuchsia::ui::composition::Allocator>();
  flatland_allocator_.set_error_handler(base::LogFidlErrorAndExitProcess(
      FROM_HERE, "fuchsia::ui::composition::Allocator"));

  // Create a transform and make it the root.
  root_transform_id_ = flatland_.NextTransformId();
  flatland_.flatland()->CreateTransform(root_transform_id_);
  flatland_.flatland()->SetRootTransform(root_transform_id_);
  primary_plane_transform_id_ = flatland_.NextTransformId();
  flatland_.flatland()->CreateTransform(primary_plane_transform_id_);

  flatland_surface_factory_->AddSurface(window, this);
}

FlatlandSurface::~FlatlandSurface() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // Signal release fences that were submitted in the last PresentImage(). This
  // is necessary because ExternalVkImageBacking destructor will wait for the
  // corresponding semaphores, while they may not be signaled by Flatland.
  for (auto& fence : release_fences_from_last_present_) {
    auto status =
        fence.signal(/*clear_mask=*/0, /*set_mask=*/ZX_EVENT_SIGNALED);
    ZX_DCHECK(status == ZX_OK, status);
  }

  flatland_surface_factory_->RemoveSurface(window_);
}

void FlatlandSurface::Present(
    scoped_refptr<gfx::NativePixmap> primary_plane_pixmap,
    std::vector<ui::OverlayPlane> overlays,
    std::vector<gfx::GpuFenceHandle> acquire_fences,
    std::vector<gfx::GpuFenceHandle> release_fences,
    SwapCompletionCallback completion_callback,
    BufferPresentedCallback presentation_callback) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (!logical_size_ || !device_pixel_ratio_) {
    pending_present_closures_.emplace_back(base::BindOnce(
        &FlatlandSurface::Present, base::Unretained(this),
        std::move(primary_plane_pixmap), std::move(overlays),
        std::move(acquire_fences), std::move(release_fences),
        std::move(completion_callback), std::move(presentation_callback)));
    return;
  }

  // Start a new frame by clearing all child transforms.
  ClearScene();

  // Prepare overlay planes.
  for (auto& overlay : overlays) {
    const auto flatland_ids = CreateOrGetFlatlandIds(
        overlay.pixmap.get(), /*is_primary_plane=*/false);
    const auto image_id = flatland_ids.image_id;
    const auto transform_id = flatland_ids.transform_id;
    const auto overlay_plane_transform = absl::get<gfx::OverlayTransform>(
        overlay.overlay_plane_data.plane_transform);

    if (overlay.gpu_fence) {
      acquire_fences.push_back(overlay.gpu_fence->GetGpuFenceHandle().Clone());
    }
    child_transforms_[overlay.overlay_plane_data.z_order] = transform_id;

    const auto rounded_bounds =
        gfx::ToRoundedRect(overlay.overlay_plane_data.display_bounds);

    const auto flatland_properties = OverlayTransformToFlatlandProperties(
        overlay_plane_transform, rounded_bounds);
    flatland_.flatland()->SetOrientation(transform_id,
                                         flatland_properties.orientation);
    flatland_.flatland()->SetTranslation(transform_id,
                                         flatland_properties.translation);
    flatland_.flatland()->SetImageDestinationSize(
        image_id,
        GfxSizeToFuchsiaSize(rounded_bounds.size(), overlay_plane_transform));

    // `crop_rect` is in normalized coordinates, but Flatland expects it to be
    // given in image coordinates.
    gfx::RectF sample_region = overlay.overlay_plane_data.crop_rect;
    const gfx::Size& buffer_size = overlay.pixmap->GetBufferSize();
    sample_region.Scale(buffer_size.width(), buffer_size.height());
    flatland_.flatland()->SetImageSampleRegion(
        image_id, {sample_region.x(), sample_region.y(), sample_region.width(),
                   sample_region.height()});
    flatland_.flatland()->SetImageBlendingFunction(
        image_id, overlay.overlay_plane_data.enable_blend
                      ? fuchsia::ui::composition::BlendMode::SRC_OVER
                      : fuchsia::ui::composition::BlendMode::SRC);
    flatland_.flatland()->SetImageOpacity(image_id,
                                          overlay.overlay_plane_data.opacity);
    flatland_.flatland()->SetImageFlip(image_id,
                                       flatland_properties.image_flip);
  }

  // Prepare primary plane.
  const auto primary_plane_image_id =
      CreateOrGetFlatlandIds(primary_plane_pixmap.get(),
                             /*is_primary_plane=*/true)
          .image_id;
  TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(
      "viz", "FlatlandSurface::Present", TRACE_ID_LOCAL(this),
      "primary_plane_image_id", primary_plane_image_id.value);
  child_transforms_[0] = primary_plane_transform_id_;
  flatland_.flatland()->SetContent(primary_plane_transform_id_,
                                   primary_plane_image_id);
  // TODO(crbug.com/42050483): We should set SRC blend mode when Chrome has a
  // reliable signal for opaque background.
  flatland_.flatland()->SetImageBlendingFunction(
      primary_plane_image_id, fuchsia::ui::composition::BlendMode::SRC_OVER);

  // Add children in z-order.
  for (auto& child : child_transforms_) {
    flatland_.flatland()->AddChild(root_transform_id_, child.second);
  }

  // We are given the overlays in physical coordinates. Allocation sizes of
  // primary plane buffers may not be equal to |logical_size_| if
  // |device_pixel_ratio_| is applied. Applying a scale at the root converts
  // these back to to the logical coordinates.
  const auto primary_plane_size = primary_plane_pixmap->GetBufferSize();
  const float root_scale =
      static_cast<float>(logical_size_->width()) / primary_plane_size.width();
  DCHECK_EQ(root_scale, static_cast<float>(logical_size_->height()) /
                            primary_plane_size.height());
  DCHECK_EQ(root_scale, 1.f / device_pixel_ratio_.value());
  flatland_.flatland()->SetScale(root_transform_id_, {root_scale, root_scale});

  // Add to pending frame to track callbacks.
  pending_frames_.emplace_back(
      primary_plane_image_id, std::move(primary_plane_pixmap),
      std::move(completion_callback), std::move(presentation_callback));

  // Keep track of release fences from last present for destructor.
  release_fences_from_last_present_.clear();
  for (auto& fence : release_fences) {
    release_fences_from_last_present_.push_back(fence.Clone().Release());
  }

  // Present to Flatland.
  fuchsia::ui::composition::PresentArgs present_args;
  present_args.set_requested_presentation_time(0);
  present_args.set_acquire_fences(
      GpuFenceHandlesToZxEvents(std::move(acquire_fences)));
  present_args.set_release_fences(
      GpuFenceHandlesToZxEvents(std::move(release_fences)));
  present_args.set_unsquashable(false);
  flatland_.Present(std::move(present_args),
                    base::BindOnce(&FlatlandSurface::OnPresentComplete,
                                   base::Unretained(this)));
}

mojo::PlatformHandle FlatlandSurface::CreateView() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  fuchsia::ui::views::ViewportCreationToken parent_token;
  fuchsia::ui::views::ViewCreationToken child_token;
  auto status = zx::channel::create(0, &parent_token.value, &child_token.value);
  DCHECK_EQ(status, ZX_OK);

  flatland_.flatland()->CreateView(std::move(child_token),
                                   parent_viewport_watcher_.NewRequest());
  parent_viewport_watcher_->GetLayout(
      fit::bind_member(this, &FlatlandSurface::OnGetLayout));
  return mojo::PlatformHandle(std::move(parent_token.value));
}

void FlatlandSurface::OnGetLayout(fuchsia::ui::composition::LayoutInfo info) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!logical_size_ || pending_present_closures_.empty());

  logical_size_ =
      gfx::Size(info.logical_size().width, info.logical_size().height);
  DCHECK_EQ(info.device_pixel_ratio().x, info.device_pixel_ratio().y);
  DCHECK_GT(info.device_pixel_ratio().x, 0.f);
  device_pixel_ratio_ = info.device_pixel_ratio().x;

  // Run |pending_present_closures_| that are waiting on |logical_size_| and
  // |device_pixel_ratio_|.
  for (auto& closure : pending_present_closures_) {
    std::move(closure).Run();
  }
  pending_present_closures_.clear();

  parent_viewport_watcher_->GetLayout(
      fit::bind_member(this, &FlatlandSurface::OnGetLayout));
}

void FlatlandSurface::RemovePixmapResources(FlatlandPixmapId ids) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  auto iter = pixmap_ids_to_flatland_ids_.find(ids);
  CHECK(iter != pixmap_ids_to_flatland_ids_.end(), base::NotFatalUntil::M130);
  flatland_.flatland()->ReleaseImage(iter->second.image_id);
  if (iter->second.transform_id.value) {
    flatland_.flatland()->ReleaseTransform(iter->second.transform_id);
  }
  pixmap_ids_to_flatland_ids_.erase(iter);
}

void FlatlandSurface::OnPresentComplete(
    base::TimeTicks actual_presentation_time,
    base::TimeDelta presentation_interval) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  TRACE_EVENT_NESTABLE_ASYNC_END1("viz", "FlatlandSurface::PresentFrame",
                                  TRACE_ID_LOCAL(this), "image_id",
                                  pending_frames_.front().image_id.value);

  auto& frame = pending_frames_.front();

  std::move(frame.completion_callback)
      .Run(gfx::SwapCompletionResult(gfx::SwapResult::SWAP_ACK));
  std::move(frame.presentation_callback)
      .Run(gfx::PresentationFeedback(actual_presentation_time,
                                     presentation_interval,
                                     gfx::PresentationFeedback::kVSync));

  pending_frames_.pop_front();
}

FlatlandSurface::FlatlandIds FlatlandSurface::CreateOrGetFlatlandIds(
    gfx::NativePixmap* pixmap,
    bool is_primary_plane) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  const auto& handle =
      static_cast<FlatlandSysmemNativePixmap*>(pixmap)->PeekHandle();
  FlatlandSysmemBufferCollection* collection =
      static_cast<FlatlandSysmemNativePixmap*>(pixmap)
          ->sysmem_buffer_collection();
  zx_koid_t buffer_collection_id = collection->id();
  const FlatlandPixmapId ids = {.buffer_collection_id = buffer_collection_id,
                                .buffer_index = handle.buffer_index};

  const auto ids_itr = pixmap_ids_to_flatland_ids_.find(ids);
  if (ids_itr != pixmap_ids_to_flatland_ids_.end()) {
    // There is a size change in pixmap, we should recreate the image with the
    // updated size.
    if (ids_itr->second.image_size != pixmap->GetBufferSize()) {
      flatland_.flatland()->ReleaseImage(ids_itr->second.image_id);
      ids_itr->second.image_id =
          CreateImage(&flatland_, collection->GetFlatlandImportToken(),
                      pixmap->GetBufferSize(), ids.buffer_index);
      ids_itr->second.image_size = pixmap->GetBufferSize();
      if (!is_primary_plane) {
        flatland_.flatland()->SetContent(ids_itr->second.transform_id,
                                         ids_itr->second.image_id);
      }
    }
    return ids_itr->second;
  }

  const fuchsia::ui::composition::ContentId image_id =
      CreateImage(&flatland_, collection->GetFlatlandImportToken(),
                  pixmap->GetBufferSize(), ids.buffer_index);

  // Skip creating a transform for the primary plane because
  // |primary_plane_transform_id_| is used.
  fuchsia::ui::composition::TransformId transform_id = {0};
  if (!is_primary_plane) {
    transform_id = flatland_.NextTransformId();
    flatland_.flatland()->CreateTransform(transform_id);
    flatland_.flatland()->SetContent(transform_id, image_id);
  }

  FlatlandSurface::FlatlandIds flatland_ids = {
      .image_id = image_id,
      .transform_id = transform_id,
      .image_size = (pixmap->GetBufferSize())};
  pixmap_ids_to_flatland_ids_[ids] = flatland_ids;
  collection->AddOnReleasedCallback(
      base::BindOnce(&FlatlandSurface::RemovePixmapResources,
                     weak_ptr_factory_.GetWeakPtr(), ids));

  return flatland_ids;
}

void FlatlandSurface::ClearScene() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  for (auto& child : child_transforms_) {
    flatland_.flatland()->RemoveChild(root_transform_id_, child.second);
  }
  child_transforms_.clear();
}

void FlatlandSurface::OnFlatlandError(
    fuchsia::ui::composition::FlatlandError error) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(ERROR) << "Flatland error: " << static_cast<int>(error);
  base::LogFidlErrorAndExitProcess(FROM_HERE,
                                   "fuchsia::ui::composition::Flatland");
}

FlatlandSurface::PresentedFrame::PresentedFrame(
    fuchsia::ui::composition::ContentId image_id,
    scoped_refptr<gfx::NativePixmap> primary_plane,
    SwapCompletionCallback completion_callback,
    BufferPresentedCallback presentation_callback)
    : image_id(image_id),
      primary_plane(primary_plane),
      completion_callback(std::move(completion_callback)),
      presentation_callback(std::move(presentation_callback)) {}
FlatlandSurface::PresentedFrame::~PresentedFrame() = default;

FlatlandSurface::PresentedFrame::PresentedFrame(PresentedFrame&&) = default;
FlatlandSurface::PresentedFrame& FlatlandSurface::PresentedFrame::operator=(
    PresentedFrame&&) = default;

}  // namespace ui