chromium/ui/ozone/platform/drm/gpu/hardware_display_plane_manager_atomic.cc

// Copyright 2014 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/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ui/ozone/platform/drm/gpu/hardware_display_plane_manager_atomic.h"

#include <sync/sync.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

#include <memory>
#include <utility>

#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/files/platform_file.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "ui/gfx/gpu_fence.h"
#include "ui/gfx/gpu_fence_handle.h"
#include "ui/ozone/platform/drm/common/drm_util.h"
#include "ui/ozone/platform/drm/gpu/crtc_controller.h"
#include "ui/ozone/platform/drm/gpu/drm_device.h"
#include "ui/ozone/platform/drm/gpu/drm_framebuffer.h"
#include "ui/ozone/platform/drm/gpu/drm_gpu_util.h"
#include "ui/ozone/platform/drm/gpu/hardware_display_plane_atomic.h"
#include "ui/ozone/platform/drm/gpu/page_flip_request.h"

namespace ui {

namespace {

gfx::GpuFenceHandle CreateMergedGpuFenceFromFDs(
    std::vector<base::ScopedFD> fence_fds) {
  base::ScopedFD merged_fd;

  for (auto& fd : fence_fds) {
    if (merged_fd.is_valid()) {
      merged_fd.reset(sync_merge("", merged_fd.get(), fd.get()));
      DCHECK(merged_fd.is_valid());
    } else {
      merged_fd = std::move(fd);
    }
  }

  gfx::GpuFenceHandle handle;
  if (merged_fd.is_valid())
    handle.Adopt(std::move(merged_fd));

  return handle;
}

std::vector<uint32_t> GetCrtcIdsOfPlanes(
    const HardwareDisplayPlaneList& plane_list) {
  std::vector<uint32_t> crtcs;
  for (HardwareDisplayPlane* plane : plane_list.plane_list) {
    HardwareDisplayPlaneAtomic* atomic_plane =
        static_cast<HardwareDisplayPlaneAtomic*>(plane);
    if (crtcs.empty() || crtcs.back() != atomic_plane->AssignedCrtcId())
      crtcs.push_back(atomic_plane->AssignedCrtcId());
  }
  return crtcs;
}

bool AddPendingCrtcProperty(drmModeAtomicReq* property_set,
                            uint32_t crtc_id,
                            DrmWrapper::Property& prop,
                            std::optional<ScopedDrmPropertyBlob>& pending_blob,
                            std::vector<ScopedDrmPropertyBlob>& pending_blobs) {
  // If `pending_blob` is std::nullopt then don't change the property.
  if (!pending_blob.has_value()) {
    return true;
  }

  // Take the pending blob. If we successfully set it, we'll add it to
  // `pending_blobs`.
  ScopedDrmPropertyBlob blob = std::move(pending_blob.value());
  pending_blob = std::nullopt;
  if (!prop.id) {
    return true;
  }

  // Update the CRTC property and add the change to the commit.
  if (blob) {
    prop.value = blob->id();
  } else {
    // If the blob was nullptr, then un-set the property.
    prop.value = 0;
  }
  int ret =
      drmModeAtomicAddProperty(property_set, crtc_id, prop.id, prop.value);
  if (ret < 0) {
    LOG(ERROR) << "Failed to set CTM property for crtc=" << crtc_id;
    return false;
  }

  // Save the pending blob in `pending_blobs` so that it can be freed after
  // `property_set` is committed.
  pending_blobs.push_back(std::move(blob));
  return true;
}

bool AddAllPendingCrtcProperties(
    drmModeAtomicReq* property_set,
    HardwareDisplayPlaneManager::CrtcState& crtc_state,
    std::vector<ScopedDrmPropertyBlob>& pending_blobs) {
  bool result = true;
  auto& crtc_props = crtc_state.properties;

  if (!AddPendingCrtcProperty(property_set, crtc_props.id, crtc_props.ctm,
                              crtc_state.pending_ctm_blob, pending_blobs)) {
    LOG(ERROR) << "Failed to set CTM property for crtc=" << crtc_props.id;
    result = false;
  }

  if (!AddPendingCrtcProperty(
          property_set, crtc_props.id, crtc_props.degamma_lut,
          crtc_state.pending_degamma_lut_blob, pending_blobs)) {
    LOG(ERROR) << "Failed to set DEGAMMA_LUT property for crtc="
               << crtc_props.id;
    result = false;
  }

  if (!AddPendingCrtcProperty(property_set, crtc_props.id, crtc_props.gamma_lut,
                              crtc_state.pending_gamma_lut_blob,
                              pending_blobs)) {
    LOG(ERROR) << "Failed to set GAMMA_LUT property for crtc=" << crtc_props.id;
    result = false;
  }

  return result;
}

// Resets |plane|'s properties if it is not found in any
// HardwareDisplayPlaneList::plane_list of |plane_lists|, as it is not currently
// being used. Returns true if the properties were reset.
bool ResetPlanePropsIfUnused(
    const base::flat_set<HardwareDisplayPlaneList*>& plane_lists,
    HardwareDisplayPlane* plane) {
  // When checking if old plane is being used for the current frame, all
  // current frames must be checked, not just the current |plane_list| as
  // the plane might have migrated to another CRTC.
  for (HardwareDisplayPlaneList* plane_list : plane_lists) {
    if (base::Contains(plane_list->plane_list, plane)) {
      return false;
    }
  }

  // |plane| is shared state between |old_plane_list| and |plane_list|.
  // When we call BeginFrame(), we reset in_use since we need to be able to
  // allocate the planes as needed. The current frame might not need to use
  // |plane|, thus |plane->in_use()| would be false even though the previous
  // frame used it. It's existence in |old_plane_list| is sufficient to
  // signal that |plane| was in use previously.
  HardwareDisplayPlaneAtomic* atomic_plane =
      static_cast<HardwareDisplayPlaneAtomic*>(plane);
  atomic_plane->AssignDisableProps();
  return true;
}

}  // namespace

HardwareDisplayPlaneManagerAtomic::HardwareDisplayPlaneManagerAtomic(
    DrmDevice* drm)
    : HardwareDisplayPlaneManager(drm) {}

HardwareDisplayPlaneManagerAtomic::~HardwareDisplayPlaneManagerAtomic() =
    default;

bool HardwareDisplayPlaneManagerAtomic::SetCrtcProps(
    drmModeAtomicReq* atomic_request,
    uint32_t crtc_id,
    bool set_active,
    uint32_t mode_id,
    bool enable_vrr) {
  // Only making a copy here to retrieve the the props IDs. The state will be
  // updated only after a successful modeset.
  CrtcProperties modeset_props = GetCrtcStateForCrtcId(crtc_id).properties;
  modeset_props.active.value = static_cast<uint64_t>(set_active);
  modeset_props.mode_id.value = mode_id;
  modeset_props.vrr_enabled.value = enable_vrr;

  bool status =
      AddPropertyIfValid(atomic_request, crtc_id, modeset_props.active);
  status &= AddPropertyIfValid(atomic_request, crtc_id, modeset_props.mode_id);
  status &=
      AddPropertyIfValid(atomic_request, crtc_id, modeset_props.vrr_enabled);
  return status;
}

bool HardwareDisplayPlaneManagerAtomic::SetConnectorProps(
    drmModeAtomicReq* atomic_request,
    uint32_t connector_id,
    uint32_t crtc_id) {
  auto connector_index = LookupConnectorIndex(connector_id);
  DCHECK(connector_index.has_value());
  // Only making a copy here to retrieve the the props IDs. The state will be
  // updated only after a successful modeset.
  ConnectorProperties connector_props = connectors_props_[*connector_index];
  connector_props.crtc_id.value = crtc_id;
  // Set link-status to DRM_MODE_LINK_STATUS_GOOD when a connector is connected
  // and has modes. In case a link training has failed and link-status is now
  // BAD, the kernel expects the userspace to reset it to GOOD; otherwise, it
  // will ignore modeset requests which have the same mode as the reported bad
  // status. However, if a connector is marked connected but has no modes, it
  // effectively has a bandwidth of 0Gbps and failed all link training
  // attempts. Leave it in link_status bad, since resetting the connector's
  // resources in DRM should not be affected by this state.
  // https://www.kernel.org/doc/html/latest/gpu/drm-kms.html#standard-connector-properties
  if (connector_props.connection == DRM_MODE_CONNECTED &&
      connector_props.count_modes != 0) {
    connector_props.link_status.value = DRM_MODE_LINK_STATUS_GOOD;
  }

  bool status =
      AddPropertyIfValid(atomic_request, connector_id, connector_props.crtc_id);
  status &= AddPropertyIfValid(atomic_request, connector_id,
                               connector_props.link_status);
  return status;
}

bool HardwareDisplayPlaneManagerAtomic::Commit(CommitRequest commit_request,
                                               uint32_t flags) {
  bool is_testing = flags & DRM_MODE_ATOMIC_TEST_ONLY;
  bool status = true;

  std::vector<ScopedDrmPropertyBlob> scoped_blobs;

  base::flat_set<HardwareDisplayPlaneList*> enable_planes_lists;
  base::flat_set<HardwareDisplayPlaneList*> all_planes_lists;

  ScopedDrmAtomicReqPtr atomic_request(drmModeAtomicAlloc());

  for (const auto& crtc_request : commit_request) {
    if (crtc_request.plane_list())
      all_planes_lists.insert(crtc_request.plane_list());

    uint32_t mode_id = 0;
    if (crtc_request.should_enable_crtc()) {
      auto mode_blob = drm_->CreatePropertyBlob(&crtc_request.mode(),
                                                sizeof(crtc_request.mode()));
      status &= (mode_blob != nullptr);
      if (mode_blob) {
        scoped_blobs.push_back(std::move(mode_blob));
        mode_id = scoped_blobs.back()->id();
      }
    }

    uint32_t crtc_id = crtc_request.crtc_id();

    status &= SetCrtcProps(atomic_request.get(), crtc_id,
                           crtc_request.should_enable_crtc(), mode_id,
                           crtc_request.enable_vrr());
    status &=
        SetConnectorProps(atomic_request.get(), crtc_request.connector_id(),
                          crtc_request.should_enable_crtc() * crtc_id);

    if (crtc_request.should_enable_crtc()) {
      DCHECK(crtc_request.plane_list());
      if (!AssignOverlayPlanes(crtc_request.plane_list(),
                               crtc_request.overlays(), crtc_id)) {
        LOG_IF(ERROR, !is_testing) << "Failed to Assign Overlay Planes";
        status = false;
      }
      enable_planes_lists.insert(crtc_request.plane_list());
    }
  }

  // TODO(markyacoub): Ideally this doesn't need to be a separate step. It
  // should all be handled in Set{Crtc,Connector,Plane}Props() modulo some state
  // tracking changes that should be done post commit. Break it apart when both
  // Commit() are consolidated.
  std::vector<ScopedDrmPropertyBlob> pending_blobs;
  SetAtomicPropsForCommit(enable_planes_lists, atomic_request.get(),
                          pending_blobs, is_testing);

  // TODO(markyacoub): failed |status|'s should be made as DCHECKs. The only
  // reason some of these would be failing is OOM. If we OOM-ed there's no point
  // in trying to recover.
  if (!status || !drm_->CommitProperties(atomic_request.get(), flags,
                                         commit_request.size(), nullptr)) {
    if (is_testing)
      VPLOG(2) << "Modeset Test is rejected.";
    else
      PLOG(ERROR) << "Failed to commit properties for modeset.";

    for (HardwareDisplayPlaneList* list : all_planes_lists)
      ResetCurrentPlaneList(list);

    return false;
  }

  if (!is_testing)
    UpdateCrtcAndPlaneStatesAfterModeset(commit_request);

  for (HardwareDisplayPlaneList* list : enable_planes_lists) {
    if (!is_testing)
      list->plane_list.swap(list->old_plane_list);
    list->plane_list.clear();
  }

  return true;
}

void HardwareDisplayPlaneManagerAtomic::SetAtomicPropsForCommit(
    const base::flat_set<HardwareDisplayPlaneList*>& plane_lists,
    drmModeAtomicReq* atomic_request,
    std::vector<ScopedDrmPropertyBlob>& pending_blobs,
    bool test_only) {
  for (HardwareDisplayPlaneList* plane_list : plane_lists) {
    // Set properties of planes being used in this commit.
    for (HardwareDisplayPlane* plane : plane_list->plane_list) {
      HardwareDisplayPlaneAtomic* atomic_plane =
          static_cast<HardwareDisplayPlaneAtomic*>(plane);
      atomic_plane->SetPlaneProps(atomic_request);
    }

    // Reset properties of old planes not being used in this commit.
    for (HardwareDisplayPlane* plane : plane_list->old_plane_list) {
      if (ResetPlanePropsIfUnused(plane_lists, plane)) {
        HardwareDisplayPlaneAtomic* atomic_plane =
            static_cast<HardwareDisplayPlaneAtomic*>(plane);
        atomic_plane->SetPlaneProps(atomic_request);
      }
    }

    const std::vector<uint32_t>& crtcs = GetCrtcIdsOfPlanes(*plane_list);
    for (uint32_t crtc : crtcs) {
      // This is actually pretty important, since these CRTC lists are generated
      // from planes who may or may not have crtcs ids set to 0 when not in use
      // (or when waiting for vblank).
      // TODO(b/189073356): See if we can use a DCHECK after we clean things up
      auto idx = LookupCrtcIndex(crtc);
      if (!idx) {
        continue;
      }

      AddPropertyIfValid(atomic_request, crtc,
                         crtc_state_[*idx].properties.background_color);
      if (base::FeatureList::IsEnabled(
              display::features::kCtmColorManagement)) {
        AddAllPendingCrtcProperties(atomic_request, crtc_state_[*idx],
                                    pending_blobs);
      }
    }

    if (test_only) {
      for (auto* plane : plane_list->plane_list) {
        plane->set_in_use(false);
      }
      for (auto* plane : plane_list->old_plane_list) {
        plane->set_in_use(true);
      }
    }
  }
}

bool HardwareDisplayPlaneManagerAtomic::Commit(
    HardwareDisplayPlaneList* plane_list,
    scoped_refptr<PageFlipRequest> page_flip_request,
    gfx::GpuFenceHandle* release_fence) {
  bool test_only = !page_flip_request;

  ScopedDrmAtomicReqPtr atomic_property_set(drmModeAtomicAlloc());
  std::vector<ScopedDrmPropertyBlob> pending_blobs;
  SetAtomicPropsForCommit(base::flat_set<HardwareDisplayPlaneList*>{plane_list},
                          atomic_property_set.get(), pending_blobs, test_only);

  // After we perform the atomic commit, and if the caller has requested an
  // out-fence, the out_fence_fds vector will contain any provided out-fence
  // fds for the crtcs, therefore the scope of out_fence_fds needs to outlive
  // the CommitProperties call. CommitProperties will write into the Receiver,
  // and the Receiver's destructor writes into ScopedFD, so we need to ensure
  // that the Receivers are destructed before we attempt to use their
  // corresponding ScopedFDs.
  std::vector<uint32_t> crtcs = GetCrtcIdsOfPlanes(*plane_list);
  std::vector<base::ScopedFD> out_fence_fds;
  {
    std::vector<base::ScopedFD::Receiver> out_fence_fd_receivers;
    if (release_fence) {
      if (!AddOutFencePtrProperties(atomic_property_set.get(),
                                    crtcs, &out_fence_fds,
                                    &out_fence_fd_receivers)) {
        ResetCurrentPlaneList(plane_list);
        return false;
      }
    }

    uint32_t flags =
        test_only ? DRM_MODE_ATOMIC_TEST_ONLY : DRM_MODE_ATOMIC_NONBLOCK;

    if (!drm_->CommitProperties(atomic_property_set.get(), flags,
                                crtcs.size(), page_flip_request)) {
      if (!test_only) {
        PLOG(ERROR) << "Failed to commit properties for page flip.";
      } else {
        VPLOG(2) << "Failed to commit properties for MODE_ATOMIC_TEST_ONLY.";
      }

      ResetCurrentPlaneList(plane_list);
      return false;
    }
  }

  if (release_fence)
    *release_fence = CreateMergedGpuFenceFromFDs(std::move(out_fence_fds));

  if (!test_only)
    plane_list->plane_list.swap(plane_list->old_plane_list);

  plane_list->plane_list.clear();
  return true;
}

bool HardwareDisplayPlaneManagerAtomic::TestSeamlessMode(
    int32_t crtc_id,
    const drmModeModeInfo& mode) {
  TRACE_EVENT1("drm", "HardwareDisplayPlaneManagerAtomic::TestSeamlessMode",
               "crtc_id", crtc_id);
  ScopedDrmAtomicReqPtr atomic_request(drmModeAtomicAlloc());

  ScopedDrmPropertyBlob mode_blob =
      drm_->CreatePropertyBlob(&mode, sizeof(mode));
  if (mode_blob == nullptr) {
    LOG(WARNING) << "Failed to create PropertyBlob for mode.";
    return false;
  }

  // Get a copy of the modeset_props. The actual state will not be updated until
  // after a successful modeset.
  CrtcProperties modeset_props = GetCrtcStateForCrtcId(crtc_id).properties;
  modeset_props.mode_id.value = mode_blob->id();

  if (!AddPropertyIfValid(atomic_request.get(), crtc_id,
                          modeset_props.mode_id)) {
    LOG(WARNING) << "Failed to add mode_id property";
    return false;
  }

  const int num_crtcs = 1;
  const uint32_t seamless_test_flags = DRM_MODE_ATOMIC_TEST_ONLY;
  return drm_->CommitProperties(atomic_request.get(), seamless_test_flags,
                                num_crtcs, nullptr);
}

bool HardwareDisplayPlaneManagerAtomic::DisableOverlayPlanes(
    HardwareDisplayPlaneList* plane_list) {
  bool ret = true;

  if (!plane_list->old_plane_list.empty()) {
    ScopedDrmAtomicReqPtr atomic_property_set(drmModeAtomicAlloc());
    for (HardwareDisplayPlane* plane : plane_list->old_plane_list) {
      plane->set_in_use(false);
      plane->set_owning_crtc(0);

      HardwareDisplayPlaneAtomic* atomic_plane =
          static_cast<HardwareDisplayPlaneAtomic*>(plane);
      atomic_plane->AssignPlaneProps(
          nullptr, 0, 0, gfx::Rect(), gfx::Rect(), gfx::Rect(),
          gfx::OVERLAY_TRANSFORM_NONE, gfx::ColorSpace(),
          base::kInvalidPlatformFile, DRM_FORMAT_INVALID, false);
      atomic_plane->SetPlaneProps(atomic_property_set.get());
    }
    ret = drm_->CommitProperties(atomic_property_set.get(),
                                 /*flags=*/0, 0 /*unused*/, nullptr);
    PLOG_IF(ERROR, !ret) << "Failed to commit properties for page flip.";
  }
  return ret;
}

bool HardwareDisplayPlaneManagerAtomic::ValidatePrimarySize(
    const DrmOverlayPlane& primary,
    const drmModeModeInfo& mode) {
  // Atomic KMS allows for primary planes that don't match the size of
  // the current mode.
  return true;
}

void HardwareDisplayPlaneManagerAtomic::RequestPlanesReadyCallback(
    DrmOverlayPlaneList planes,
    base::OnceCallback<void(DrmOverlayPlaneList planes)> callback) {
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(planes)));
}

bool HardwareDisplayPlaneManagerAtomic::SetPlaneData(
    HardwareDisplayPlaneList*,
    HardwareDisplayPlane* hw_plane,
    const DrmOverlayPlane& overlay,
    uint32_t crtc_id,
    const gfx::Rect& src_rect) {
  HardwareDisplayPlaneAtomic* atomic_plane =
      static_cast<HardwareDisplayPlaneAtomic*>(hw_plane);
  uint32_t framebuffer_id = overlay.enable_blend
                                ? overlay.buffer->framebuffer_id()
                                : overlay.buffer->opaque_framebuffer_id();
  int fence_fd = base::kInvalidPlatformFile;

  if (overlay.gpu_fence) {
    const auto& gpu_fence_handle = overlay.gpu_fence->GetGpuFenceHandle();
    fence_fd = gpu_fence_handle.Peek();
  }

  if (!atomic_plane->AssignPlaneProps(
          drm_, crtc_id, framebuffer_id, overlay.display_bounds, src_rect,
          overlay.damage_rect, overlay.plane_transform, overlay.color_space,
          fence_fd, overlay.buffer->framebuffer_pixel_format(),
          overlay.buffer->is_original_buffer())) {
    return false;
  }
  return true;
}

bool HardwareDisplayPlaneManagerAtomic::InitializePlanes() {
  ScopedDrmPlaneResPtr plane_resources = drm_->GetPlaneResources();
  if (!plane_resources) {
    PLOG(ERROR) << "Failed to get plane resources.";
    return false;
  }

  for (uint32_t i = 0; i < plane_resources->count_planes; ++i) {
    std::unique_ptr<HardwareDisplayPlane> plane(
        CreatePlane(plane_resources->planes[i]));

    if (plane->Initialize(drm_))
      planes_.push_back(std::move(plane));
  }

  return true;
}

std::unique_ptr<HardwareDisplayPlane>
HardwareDisplayPlaneManagerAtomic::CreatePlane(uint32_t plane_id) {
  return std::make_unique<HardwareDisplayPlaneAtomic>(plane_id);
}

bool HardwareDisplayPlaneManagerAtomic::CommitPendingCrtcState(
    CrtcState& crtc_state) {
  std::vector<ScopedDrmPropertyBlob> pending_blobs;
  ScopedDrmAtomicReqPtr property_set(drmModeAtomicAlloc());

  bool result = AddAllPendingCrtcProperties(property_set.get(), crtc_state,
                                            pending_blobs);

  // If we aren't committing any new blobs, early-out.
  if (pending_blobs.empty()) {
    return result;
  }

  // If we try to do this in a non-blocking fashion this can return EBUSY since
  // there is a pending page flip. Do a blocking commit (the same as the legacy
  // API) to ensure the properties are applied.
  // TODO(dnicoara): Should cache these values locally and aggregate them with
  // the page flip event otherwise this "steals" a vsync to apply the property.
  if (!drm_->CommitProperties(property_set.get(), 0, 0, nullptr)) {
    LOG(ERROR) << "Failed to commit properties for crtc="
               << crtc_state.properties.id;
    result = false;
  }

  return result;
}

bool HardwareDisplayPlaneManagerAtomic::AddOutFencePtrProperties(
    drmModeAtomicReq* property_set,
    const std::vector<uint32_t>& crtcs,
    std::vector<base::ScopedFD>* out_fence_fds,
    std::vector<base::ScopedFD::Receiver>* out_fence_fd_receivers) {
  // Reserve space in vector to ensure no reallocation will take place
  // and thus all pointers to elements will remain valid
  DCHECK(out_fence_fds->empty());
  DCHECK(out_fence_fd_receivers->empty());
  out_fence_fds->reserve(crtcs.size());
  out_fence_fd_receivers->reserve(crtcs.size());

  for (uint32_t crtc : crtcs) {
    const auto crtc_index = LookupCrtcIndex(crtc);
    DCHECK(crtc_index.has_value());
    const auto out_fence_ptr_id =
        crtc_state_[*crtc_index].properties.out_fence_ptr.id;

    if (out_fence_ptr_id > 0) {
      out_fence_fds->push_back(base::ScopedFD());
      out_fence_fd_receivers->emplace_back(out_fence_fds->back());
      // Add the OUT_FENCE_PTR property pointing to the memory location
      // to save the out-fence fd into for this crtc. Note that
      // the out-fence fd is produced only after we perform the atomic
      // commit, so we need to ensure that the pointer remains valid
      // until then.
      int ret = drmModeAtomicAddProperty(
          property_set, crtc, out_fence_ptr_id,
          reinterpret_cast<uint64_t>(out_fence_fd_receivers->back().get()));
      if (ret < 0) {
        LOG(ERROR) << "Failed to set OUT_FENCE_PTR property for crtc=" << crtc
                   << " error=" << -ret;
        out_fence_fd_receivers->pop_back();
        out_fence_fds->pop_back();
        return false;
      }
    }
  }

  return true;
}

}  // namespace ui