chromium/ui/gl/delegated_ink_point_renderer_gpu.cc

// Copyright 2024 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/gl/delegated_ink_point_renderer_gpu.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <utility>

#include "base/debug/dump_without_crashing.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/win/windows_version.h"
#include "ui/gfx/geometry/rect_conversions.h"

namespace gl {

namespace {

// Maximum number of pointer ids that will be considered for drawing. Number is
// chosen arbitrarily, as it seems unlikely that the total number of input
// sources using delegated ink trails would exceed 10, but it can be raised if
// the need arises.
constexpr uint64_t kMaximumNumberOfPointerIds = 10;

// Note that this returns true if the HRESULT is anything other than S_OK,
// meaning that it returns true when an event is traced (because of a
// failure).
bool TraceEventOnFailure(HRESULT hr, const char* name) {
  if (SUCCEEDED(hr)) {
    return false;
  }

  TRACE_EVENT_INSTANT1("delegated_ink_trails", name, TRACE_EVENT_SCOPE_THREAD,
                       "hr", hr);
  return true;
}

}  // namespace

DelegatedInkPointRendererGpu::DelegatedInkPointRendererGpu() = default;
DelegatedInkPointRendererGpu::~DelegatedInkPointRendererGpu() = default;

void DelegatedInkPointRendererGpu::InitMessagePipeline(
    mojo::PendingReceiver<gfx::mojom::DelegatedInkPointRenderer>
        pending_receiver) {
  // The remote end of this pipeline exists on a per-tab basis, so if tab A
  // is using the feature and then tab B starts trying to use it, a new
  // PendingReceiver will arrive here while |receiver_| is still bound to the
  // remote in tab A. Because of this possibility, we unconditionally reset
  // |receiver_| before trying to bind anything here. This is fine to do
  // because we only ever need to actively receive points from one tab as only
  // one tab can be in the foreground and actively inking at a time.
  receiver_.reset();

  receiver_.Bind(std::move(pending_receiver));
}

bool DelegatedInkPointRendererGpu::Initialize(
    IDCompositionDevice2* dcomp_device2,
    IDXGISwapChain1* root_swap_chain) {
  if (swap_chain_ == root_swap_chain && delegated_ink_trail_) {
    return true;
  }

  swap_chain_ = root_swap_chain;

  if (!ink_trail_device_ &&
      TraceEventOnFailure(
          dcomp_device2->QueryInterface(IID_PPV_ARGS(&ink_trail_device_)),
          "DelegatedInkPointRendererGpu::Initialize - "
          "DCompDevice2 as InkTrailDevice failed")) {
    return false;
  }

  if (TraceEventOnFailure(
          ink_trail_device_->CreateDelegatedInkTrailForSwapChain(
              root_swap_chain, &delegated_ink_trail_),
          "DelegatedInkPointRendererGpu::Initialize - Failed to create "
          "delegated ink trail.")) {
    return false;
  }

  // Start a new trail if the renderer needs to be (re)initialized.
  force_new_ink_trail_ = true;

  return true;
}

bool DelegatedInkPointRendererGpu::DelegatedInkIsSupported(
    const Microsoft::WRL::ComPtr<IDCompositionDevice2>& dcomp_device) const {
  const base::win::OSInfo::VersionNumber& os_version =
      base::win::OSInfo::GetInstance()->version_number();
  // Win11 24H2 is first introduced in insider build #26100. Issues related to
  // the delegated ink trail API, such as flickering in the top 3rd of the
  // screen are addressed in 24H2.
  // TODO(crbug.com/40153696) Add 24H2 to base::win::Version.
  const bool is_24h2_or_greater =
      (os_version.major > 10) ||
      (os_version.major == 10 && os_version.build >= 26100);

  if (!is_24h2_or_greater) {
    return false;
  }

  Microsoft::WRL::ComPtr<IDCompositionInkTrailDevice> ink_trail_device;
  HRESULT hr = dcomp_device.As(&ink_trail_device);
  return hr == S_OK;
}

uint64_t DelegatedInkPointRendererGpu::GetMaximumNumberOfPointerIdsForTesting()
    const {
  CHECK_IS_TEST();
  return kMaximumNumberOfPointerIds;
}

void DelegatedInkPointRendererGpu::ReportPointsDrawn() {
  const base::TimeTicks now = base::TimeTicks::Now();
  // If there is a point that matches the metadata and the histogram has not yet
  // been fired, then this is the first frame that the metadata point will be
  // painted via the JS API.
  if (metadata_paint_time_.has_value()) {
    base::UmaHistogramCustomTimes(
        "Renderer.DelegatedInkTrail.OS.TimeFromDelegatedInkToApiPaint",
        now - metadata_paint_time_.value(), base::Milliseconds(1),
        base::Seconds(1), 50);
    metadata_paint_time_ = std::nullopt;
  }

  if (points_to_be_drawn_.empty()) {
    return;
  }

  CHECK(metadata_);
  base::TimeTicks most_recent_timestamp = base::TimeTicks::Min();
  for (const auto& point : points_to_be_drawn_) {
    UMA_HISTOGRAM_TIMES("Renderer.DelegatedInkTrail.OS.TimeToDrawPointsMillis",
                        now - point.timestamp());
    most_recent_timestamp = std::max(point.timestamp(), most_recent_timestamp);

    // Update the point's `paint_timestamp` if this is the first time it is
    // being painted so that it can later be compared with the metadata's first
    // paint time.
    DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
        delegated_ink_points_[point.pointer_id()];
    auto trail_point_it = token_map.find(point);
    if (trail_point_it != token_map.end()) {
      gfx::DelegatedInkPoint& trail_point = trail_point_it->first;
      if (!trail_point.paint_timestamp().has_value()) {
        trail_point.set_paint_timestamp(now);
      }
    }
  }

  CHECK_GE(most_recent_timestamp, metadata_->timestamp());
  base::UmaHistogramTimes(
      "Renderer.DelegatedInkTrail.LatencyImprovement.OS.WithoutPrediction",
      most_recent_timestamp - metadata_->timestamp());
  base::UmaHistogramCounts100(
      "Renderer.DelegatedInkTrail.OS.OutstandingPointsToDraw",
      points_to_be_drawn_.size());

  points_to_be_drawn_.clear();
}

void DelegatedInkPointRendererGpu::SetDelegatedInkTrailStartPoint(
    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
  TRACE_EVENT_WITH_FLOW1(
      "delegated_ink_trails",
      "DelegatedInkPointRendererGpu::SetDelegatedInkTrailStartPoint",
      TRACE_ID_GLOBAL(metadata->trace_id()), TRACE_EVENT_FLAG_FLOW_IN,
      "metadata", metadata->ToString());

  DCHECK(delegated_ink_trail_);

  // If certain conditions are met, we can simply erase the trail up to the
  // point matching |metadata|, instead of starting a new trail. These
  // conditions include:
  //  1. A trail has already been started, meaning that |metadata_| and
  //     |pointer_id_| exist.
  //  2. The current |metadata_| and |metadata| both have the same color, so
  //     the color of the trail does not need to change.
  //  3. |force_new_ink_trail_| is false - renderer has not been
  //     re-initialized.
  // (continued below)
  if (!force_new_ink_trail_ && metadata_ &&
      metadata->color() == metadata_->color() && pointer_id_) {
    DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
        delegated_ink_points_[pointer_id_.value()];
    auto point_matching_metadata_it = token_map.find(
        gfx::DelegatedInkPoint(metadata->point(), metadata->timestamp()));
    // (continued from above)
    //  4. We have a DelegatedInkPoint with a timestamp equal to or greater
    //     than |metadata|'s timestamp in the token map associated with
    //     |pointer_id_|.
    // (continued below)
    if (point_matching_metadata_it != token_map.end()) {
      gfx::DelegatedInkPoint point_matching_metadata =
          point_matching_metadata_it->first;
      std::optional<unsigned int> token = point_matching_metadata_it->second;
      // (continued from above)
      //  5. The DelegatedInkPoint retrieved above matches |metadata| - this
      //     means that the timestamp is an exact match and the point is
      //     acceptably close.
      //  6. The token associated with this DelegatedInkPoint is valid,
      //     meaning that the point has previously been drawn as part of a
      //     trail.
      //
      // If all of these conditions are met, then we can attempt to just
      // remove points from the trail instead of starting a new trail
      // altogether.
      if (point_matching_metadata.MatchesDelegatedInkMetadata(metadata.get()) &&
          token) {
        metadata_paint_time_ = point_matching_metadata.paint_timestamp();
        bool remove_trail_points_failed = TraceEventOnFailure(
            delegated_ink_trail_->RemoveTrailPoints(token.value()),
            "DelegatedInkPointRendererGpu::SetDelegatedInkTrailStartPoint - "
            "Failed to remove trail points.");
        if (!remove_trail_points_failed) {
          // Remove all points up to and including the point that matches
          // |metadata|. No need to hold on to the point that matches metadata
          // because we've already added it to AddTrailPoints previously, and
          // the next valid |metadata| is guaranteed to be after it.
          token_map.erase(token_map.begin(),
                          std::next(point_matching_metadata_it));
          // Ensure that points that are being removed from the trail are not
          // being reported as painted in `ReportPointsDrawn()`.
          points_to_be_drawn_.erase(
              std::remove_if(points_to_be_drawn_.begin(),
                             points_to_be_drawn_.end(),
                             [&](const gfx::DelegatedInkPoint& x) {
                               return metadata->timestamp() > x.timestamp();
                             }),
              points_to_be_drawn_.end());
          metadata_ = std::move(metadata);
          return;
        }
      }
    }
  }

  D2D1_COLOR_F d2d1_color;
  const float kMaxRGBAValue = 255.f;
  d2d1_color.a = SkColorGetA(metadata->color()) / kMaxRGBAValue;
  d2d1_color.r = SkColorGetR(metadata->color()) / kMaxRGBAValue;
  d2d1_color.g = SkColorGetG(metadata->color()) / kMaxRGBAValue;
  d2d1_color.b = SkColorGetB(metadata->color()) / kMaxRGBAValue;
  if (TraceEventOnFailure(
          delegated_ink_trail_->StartNewTrail(d2d1_color),
          "DelegatedInkPointRendererGpu::SetDelegatedInkTrailStartPoint - "
          "Failed to start a new trail.")) {
    return;
  }

  points_to_be_drawn_.clear();
  wait_for_new_trail_to_draw_ = false;
  metadata_ = std::move(metadata);
  DrawSavedTrailPoints();
  force_new_ink_trail_ = false;
}

void DelegatedInkPointRendererGpu::StoreDelegatedInkPoint(
    const gfx::DelegatedInkPoint& point) {
  TRACE_EVENT_WITH_FLOW1("delegated_ink_trails",
                         "DelegatedInkPointRendererGpu::StoreDelegatedInkPoint",
                         TRACE_ID_GLOBAL(point.trace_id()),
                         TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
                         "point", point.ToString());

  const int32_t pointer_id = point.pointer_id();

  DCHECK(delegated_ink_points_.find(pointer_id) ==
             delegated_ink_points_.end() ||
         point.timestamp() >
             delegated_ink_points_[pointer_id].rbegin()->first.timestamp());

  if (metadata_ && point.timestamp() < metadata_->timestamp()) {
    return;
  }

  DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
      delegated_ink_points_[pointer_id];
  // Always save the point so that it can be drawn later. This allows points
  // that arrive before the first metadata makes it here to be drawn after
  // StartNewTrail is called. If we get to the maximum number of points that
  // we will store, start erasing the oldest ones first. This matches what the
  // OS compositor does internally when it hits the max number of points.
  if (token_map.size() == gfx::kMaximumNumberOfDelegatedInkPoints) {
    token_map.erase(token_map.begin());
  }
  token_map.insert({point, std::nullopt});

  EraseExcessPointerIds();

  if (pointer_id_ && pointer_id_.value() == pointer_id) {
    DrawDelegatedInkPoint(point);
  } else if (!pointer_id_ && metadata_) {
    DrawSavedTrailPoints();
  }
}

void DelegatedInkPointRendererGpu::ResetPrediction() {
  // Don't reset |metadata_| here so that RemoveTrailPoints() can continue
  // to be called as the final metadata(s) arrive.
  // TODO(crbug.com/40118757): Start predicting points and reset it here.
  wait_for_new_trail_to_draw_ = true;
}

uint64_t DelegatedInkPointRendererGpu::InkTrailTokenCountForTesting() const {
  CHECK_IS_TEST();
  DCHECK_EQ(delegated_ink_points_.size(), 1u);
  uint64_t valid_tokens = 0u;
  for (const auto& it : delegated_ink_points_.begin()->second) {
    if (it.second) {
      valid_tokens++;
    }
  }
  return valid_tokens;
}

bool DelegatedInkPointRendererGpu::CheckForPointerIdForTesting(
    int32_t pointer_id) const {
  CHECK_IS_TEST();
  return delegated_ink_points_.find(pointer_id) != delegated_ink_points_.end();
}

void DelegatedInkPointRendererGpu::EraseExcessPointerIds() {
  auto token_map_it = delegated_ink_points_.begin();
  while (token_map_it != delegated_ink_points_.end()) {
    const DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
        token_map_it->second;
    if (token_map.empty()) {
      token_map_it = delegated_ink_points_.erase(token_map_it);
    } else {
      token_map_it++;
    }
  }

  // In order to get to the maximum pointer id limit, sometimes one token map
  // will need to be removed even if it has a valid DelegatedInkPoint. In this
  // case, we'll remove the token map that has gone the longest without
  // receiving a new point - similar to a least recently used eviction policy.
  // This would also mean that the token map that would result in the smallest
  // latency improvement if it were drawn is being removed. The only exception
  // to this is if the above heuristic would result in the token map that has
  // a pointer id matching |pointer_id_| being removed. Since |pointer_id_|
  // matches the trail that is currently being drawn to the screen, we prefer
  // to save it so we can try to continue that trail. In this case, just
  // remove the token map with the oldest new point that is not currently
  // being drawn.
  if (delegated_ink_points_.size() > kMaximumNumberOfPointerIds) {
    std::vector<std::pair<base::TimeTicks, int32_t>>
        earliest_time_and_pointer_id;
    for (const auto& it : delegated_ink_points_) {
      earliest_time_and_pointer_id.emplace_back(
          it.second.rbegin()->first.timestamp(), it.first);
    }

    std::sort(earliest_time_and_pointer_id.begin(),
              earliest_time_and_pointer_id.end());

    uint64_t pointer_id_to_remove = 0;
    while (pointer_id_to_remove < earliest_time_and_pointer_id.size() &&
           pointer_id_ &&
           earliest_time_and_pointer_id[pointer_id_to_remove].second ==
               pointer_id_.value()) {
      pointer_id_to_remove++;
    }

    CHECK_LT(pointer_id_to_remove, earliest_time_and_pointer_id.size());
    delegated_ink_points_.erase(
        earliest_time_and_pointer_id[pointer_id_to_remove].second);
  }

  DCHECK_LE(delegated_ink_points_.size(), kMaximumNumberOfPointerIds);
}

std::optional<int32_t> DelegatedInkPointRendererGpu::GetPointerIdForMetadata() {
  if (pointer_id_ && delegated_ink_points_.find(pointer_id_.value()) !=
                         delegated_ink_points_.end()) {
    // Since we remove all DelegatedInkPoints with timestamp before
    // |metadata_|'s before calling GetPointerIdForMetadata(), we know we can
    // just grab the first DelegatedInkPoint matching |pointer_id_|.
    const gfx::DelegatedInkPoint& point_matching_metadata =
        delegated_ink_points_[pointer_id_.value()].begin()->first;
    if (point_matching_metadata.MatchesDelegatedInkMetadata(metadata_.get())) {
      return pointer_id_;
    }
  }

  pointer_id_ = std::nullopt;

  for (auto token_map_it = delegated_ink_points_.begin();
       token_map_it != delegated_ink_points_.end() && !pointer_id_;
       ++token_map_it) {
    int32_t potential_pointer_id = token_map_it->first;
    const DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
        token_map_it->second;
    DCHECK(!token_map.empty());
    if (token_map.begin()->first.MatchesDelegatedInkMetadata(metadata_.get())) {
      DCHECK(!pointer_id_);
      pointer_id_ = potential_pointer_id;
    }
  }

  return pointer_id_;
}

void DelegatedInkPointRendererGpu::DrawSavedTrailPoints() {
  DCHECK(metadata_);
  TRACE_EVENT0("delegated_ink_trails", "DrawSavedTrailPoints");

  // Remove all points that have a timestamp earlier than |metadata_|'s, since
  // we know that we won't need to draw them. This is subtly different than
  // the erasing done in SetDelegatedInkTrailStartPoint() - there the point
  // matching the metadata is removed, here it is not. The reason for this
  // difference is because there we know that it matches |metadata_| and
  // therefore it cannot possibly be drawn again, so it is safe to remove.
  // Here however, we are erasing all points up to, but not including, the
  // first point with a timestamp equal to or greater than |metadata_|'s so
  // that we can then check that point to confirm that it matches |metadata_|
  // before deciding to draw an ink trail. The point could then be erased
  // after it is successfully checked against |metadata_| and drawn, it just
  // isn't for simplicity's sake.
  for (auto& it : delegated_ink_points_) {
    DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
        it.second;
    token_map.erase(token_map.begin(),
                    token_map.lower_bound(gfx::DelegatedInkPoint(
                        metadata_->point(), metadata_->timestamp())));
  }

  EraseExcessPointerIds();

  std::optional<unsigned int> pointer_id = GetPointerIdForMetadata();

  // Now, the very first point must match |metadata_|, and as long as it does
  // we can continue to draw everything else. If at any point something can't
  // or fails to draw though, don't attempt to draw anything after it so that
  // the trail can match the user's actual stroke.
  if (pointer_id && delegated_ink_points_.find(pointer_id.value()) !=
                        delegated_ink_points_.end()) {
    DelegatedInkPointRendererGpu::DelegatedInkPointTokenMap& token_map =
        delegated_ink_points_[pointer_id.value()];
    if (!token_map.empty() &&
        token_map.begin()->first.MatchesDelegatedInkMetadata(metadata_.get())) {
      for (const auto& it : token_map) {
        if (!DrawDelegatedInkPoint(it.first)) {
          break;
        }
      }
    }
  } else {
    TRACE_EVENT_INSTANT0("delegated_ink_trails",
                         "DrawSavedTrailPoints failed - no pointer id",
                         TRACE_EVENT_SCOPE_THREAD);
  }
}

std::unique_ptr<DCLayerOverlayParams>
DelegatedInkPointRendererGpu::MakeDelegatedInkOverlay(
    IDCompositionDevice2* dcomp_device2,
    IDXGISwapChain1* root_swap_chain,
    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
  if (!Initialize(dcomp_device2, root_swap_chain)) {
    return nullptr;
  }
  auto ink_layer = std::make_unique<DCLayerOverlayParams>();
  // Ink trail should be rendered on top of all content.
  ink_layer->z_order = INT_MAX;
  const gfx::Rect presentation_rect =
      gfx::ToEnclosedRect(metadata->presentation_area());
  const gfx::Size presentation_area_enclosed_size =
      gfx::Size(presentation_rect.width(), presentation_rect.height());
  ink_layer->quad_rect = gfx::Rect(presentation_area_enclosed_size);
  ink_layer->content_rect = gfx::RectF(presentation_area_enclosed_size);
  // If (0,0) of a visual is clipped out, it can result in delegated ink not
  // being drawn at all. This is more common when DComp Surfaces are enabled,
  // but doesn't negatively impact things when the swapchain is used, so just
  // offset the visual instead of clipping the top left corner in all cases.
  ink_layer->clip_rect = std::make_optional<gfx::Rect>(
      0, 0, presentation_rect.right(), presentation_rect.bottom());
  ink_layer->transform = gfx::Transform::MakeTranslation(
      metadata->presentation_area().OffsetFromOrigin());
  ink_layer->overlay_image = DCLayerOverlayImage(
      presentation_area_enclosed_size, delegated_ink_trail_);
  SetDelegatedInkTrailStartPoint(std::move(metadata));
  return ink_layer;
}

bool DelegatedInkPointRendererGpu::DrawDelegatedInkPoint(
    const gfx::DelegatedInkPoint& point) {
  // Always wait for a new trail to be started before attempting to draw
  // anything, even if |metadata_| exists.
  if (wait_for_new_trail_to_draw_) {
    return false;
  }

  DCHECK(metadata_);
  if (!metadata_->presentation_area().Contains(point.point())) {
    return false;
  }

  DCHECK(delegated_ink_trail_);

  DCompositionInkTrailPoint ink_point;
  ink_point.radius = metadata_->diameter() / 2.f;
  // In order to account for the visual offset, the point must be offset in
  // the opposite direction.
  ink_point.x = point.point().x() - metadata_->presentation_area().x();
  ink_point.y = point.point().y() - metadata_->presentation_area().y();
  unsigned int token;

  // AddTrailPoints() can accept and draw more than one InkTrailPoint per
  // call. However, all the points get lumped together in the one token then,
  // which means that they can only be removed all together. This may be fine
  // in some scenarios, but in the vast majority of cases we will need to
  // remove one point at a time, so we choose to only add one InkTrailPoint at
  // a time.
  if (TraceEventOnFailure(
          delegated_ink_trail_->AddTrailPoints(&ink_point,
                                               /*inkPointsCount*/ 1, &token),
          "DelegatedInkPointRendererGpu::DrawDelegatedInkPoint - Failed to "
          "add trail point")) {
    // TODO(crbug.com/40118757): Start predicting points.
    return false;
  }

  TRACE_EVENT_WITH_FLOW1("delegated_ink_trails",
                         "DelegatedInkPointRendererGpu::DrawDelegatedInkPoint "
                         "- Point added to trail",
                         TRACE_ID_GLOBAL(point.trace_id()),
                         TRACE_EVENT_FLAG_FLOW_IN, "point", point.ToString());

  if (point.timestamp().IsHighResolution() &&
      point.timestamp().IsConsistentAcrossProcesses()) {
    points_to_be_drawn_.push_back(point);
  }
  delegated_ink_points_[point.pointer_id()][point] = token;
  return true;
}

void DelegatedInkPointRendererGpu::InitializeForTesting(
    IDCompositionDevice2* dcomp_device2) {
  CHECK_IS_TEST();
  Initialize(dcomp_device2, nullptr);
}

}  // namespace gl