// Copyright 2016 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/accelerated_widget_mac/ca_layer_tree_coordinator.h"

#import <AVFoundation/AVFoundation.h>

#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "build/ios_buildflags.h"
#include "ui/accelerated_widget_mac/ca_renderer_layer_tree.h"
#include "ui/base/cocoa/animation_utils.h"
#include "ui/base/cocoa/remote_layer_api.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/ca_layer_params.h"
#include "ui/gfx/mac/mtl_shared_event_fence.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_fence.h"
#include "ui/gl/gl_implementation.h"
#include "ui/gl/scoped_make_current.h"

#if BUILDFLAG(IS_MAC)
#include "ui/display/mac/display_link_mac.h"
#include "ui/gl/gl_context.h"
#endif

namespace ui {

CALayerTreeCoordinator::CALayerTreeCoordinator(
    bool allow_av_sample_buffer_display_layer,
    BufferPresentedCallback buffer_presented_callback,
    GLMakeCurrentCallback gl_make_current_callback,
    id<MTLDevice> metal_device)
    : allow_remote_layers_(ui::RemoteLayerAPISupported()),
      allow_av_sample_buffer_display_layer_(
          allow_av_sample_buffer_display_layer),
      buffer_presented_callback_(buffer_presented_callback),
      gl_make_current_callback_(gl_make_current_callback),
      metal_device_(metal_device) {}

CALayerTreeCoordinator::~CALayerTreeCoordinator() {
  // If the front frame has already been committed, its CALayer tree is active.
  // We must explicitly pop and destroy it here under a ScopedCAActionDisabler
  // to prevent triggering implicit CoreAnimation animations or transactions
  // during destruction of the active layers, which can cause a crash in
  // [ca_layer_ removeFromSuperlayer].
  if (!presented_frames_.empty() && presented_frames_.front().has_committed &&
      presented_frames_.front().layer_tree) {
    ScopedCAActionDisabler disabler;
    presented_frames_.pop();
  }
}

void CALayerTreeCoordinator::Resize(const gfx::Size& pixel_size,
                                    float scale_factor) {
#if BUILDFLAG(IS_MAC)
  has_resized_since_last_swap_ |=
      pixel_size != pixel_size_ || scale_factor_ != scale_factor;
#endif
  pixel_size_ = pixel_size;
  scale_factor_ = scale_factor;
}

void CALayerTreeCoordinator::SetCALayerErrorCode(
    gfx::CALayerResult ca_layer_error_code) {
  ca_layer_error_code_ = ca_layer_error_code;
}

CARendererLayerTree* CALayerTreeCoordinator::GetPendingCARendererLayerTree() {
  if (!unpresented_ca_renderer_layer_tree_) {
    CHECK_LE(presented_frames_.size(), presented_ca_layer_trees_max_length_);

    unpresented_ca_renderer_layer_tree_ = std::make_unique<CARendererLayerTree>(
        allow_av_sample_buffer_display_layer_, false, metal_device_);
  }
  return unpresented_ca_renderer_layer_tree_.get();
}

void CALayerTreeCoordinator::EnqueueBackpressureFences(
    std::vector<gfx::MTLSharedEventFence> metal_fences) {
  pending_backpressure_metal_fences_.insert(
      pending_backpressure_metal_fences_.end(),
      std::make_move_iterator(metal_fences.begin()),
      std::make_move_iterator(metal_fences.end()));
}

void CALayerTreeCoordinator::ApplyBackpressure() {
  // No frame has been committed yet - this is the first frame being presented.
  if (presented_frames_.empty() || !presented_frames_.front().has_committed) {
    return;
  }

  TRACE_EVENT0("gpu", "CALayerTreeCoordinator::ApplyBackpressure");

  // Apply back pressure to the previous frame.
  auto metal_fences =
      std::move(presented_frames_.front().backpressure_metal_fences);

  // Waiting on the previous frame's fence (to maximize CPU and GPU execution
  // overlap). Poll for all Metal shared events to be signaled with a 1ms delay.
  bool fences_signaled = false;
  while (!fences_signaled) {
    TRACE_EVENT0("gpu", "CALayerTreeCoordinator::ApplyBackpressure::Metal");
    fences_signaled = true;
    {
      for (const auto& fence : metal_fences) {
        if (!fence.HasSignaled()) {
          fences_signaled = false;
          break;
        }
      }
    }
    if (!fences_signaled) {
      base::PlatformThread::Sleep(base::Milliseconds(1));
    }
  }

  if (auto gl_fence =
          std::move(presented_frames_.front().backpressure_gl_fence)) {
    CHECK_NE(gl::GetANGLEImplementation(), gl::ANGLEImplementation::kMetal);
    if (gl_make_current_callback_.Run()) {
      TRACE_EVENT0("gpu", "CALayerTreeCoordinator::ApplyBackpressure::GL");
      gl_fence->ClientWait();
    } else {
      DLOG(ERROR) << "Failed to make GL context current for waiting on "
                     "backpressure GL fence";
    }
  }
}

void CALayerTreeCoordinator::Present(
    gl::Presenter::SwapCompletionCallback completion_callback,
    gl::Presenter::PresentationCallback presentation_callback) {
  std::unique_ptr<gl::GLFence> gl_fence;
  if (gl::GetANGLEImplementation() != gl::ANGLEImplementation::kMetal) {
    if (gl_make_current_callback_.Run()) {
      gl_fence = gl::GLFence::Create();
    } else {
      DLOG(ERROR) << "Failed to make GL context current for creating "
                     "backpressure GL fence";
    }
  }

  PresentedFrame frame;
  frame.completion_callback = std::move(completion_callback);
  frame.presentation_callback = std::move(presentation_callback);
  frame.backpressure_metal_fences = gfx::MTLSharedEventFence::Reduce(
      std::move(pending_backpressure_metal_fences_));
  frame.backpressure_gl_fence = std::move(gl_fence);
  frame.ca_layer_error_code = ca_layer_error_code_;
  frame.ready_timestamp = base::TimeTicks::Now();
  frame.layer_tree = std::move(unpresented_ca_renderer_layer_tree_);

  presented_frames_.push(std::move(frame));
}

void CALayerTreeCoordinator::EnsureCAContextAndRootLayer() {
  if (!allow_remote_layers_) {
    return;
  }
  if (!root_ca_layer_) {
    root_ca_layer_ = [[CALayer alloc] init];
#if BUILDFLAG(IS_MAC)
    // iOS' UIKit has default coordinate system where the origin is at the
    // upper left of the drawing area. In contrast, AppKit and Core Graphics
    // that macOS uses has its origin at the lower left of the drawing area.
    // Thus, we don't need to flip the coordinate system on iOS as it's
    // already set the way we want it to be.
    root_ca_layer_.geometryFlipped = YES;
#endif
    root_ca_layer_.opaque = YES;
  }
  if (!ca_context_) {
#if !BUILDFLAG(IS_IOS) || BUILDFLAG(IS_IOS_TVOS)
    // Create the CAContext to send this to the GPU process, and the layer
    // for the context.
#if BUILDFLAG(IS_MAC)
    CGSConnectionID connection_id = CGSMainConnectionID();
    ca_context_ = [CAContext contextWithCGSConnection:connection_id
                                              options:@{}];
#else
    // Use a very large display ID to ensure that the context is never put
    // on-screen without being explicitly parented.
    ca_context_ = [CAContext remoteContextWithOptions:@{
      kCAContextIgnoresHitTest : @YES,
      kCAContextDisplayId : @10000
    }];
#endif
    ca_context_.layer = root_ca_layer_;
#endif  // !BUILDFLAG(IS_IOS) || BUILDFLAG(IS_IOS_TVOS)
  }
}

void CALayerTreeCoordinator::CommitPresentedFrameToCA(
    base::TimeDelta frame_interval,
    base::TimeTicks display_time) {
  // Update the CALayer hierarchy.
  ScopedCAActionDisabler disabler;

  // Remove the committed frame which is displayed on the screen from the
  // |presented_frames_| queue;
  std::unique_ptr<CARendererLayerTree> current_tree;
  if (!presented_frames_.empty() && presented_frames_.front().has_committed) {
    current_tree.swap(presented_frames_.front().layer_tree);
    presented_frames_.pop();
  }

  if (presented_frames_.empty()) {
    TRACE_EVENT0("gpu", "Blank frame: No overlays or CALayers");
    DLOG(WARNING) << "Blank frame: No overlays or CALayers";
    root_ca_layer_.sublayers = nil;
    return;
  }

  gfx::CALayerParams params;
  bool no_post_task_for_callback = true;
#if BUILDFLAG(IS_MAC)
  no_post_task_for_callback = ui::SkipPostTaskForCallbacks();
  if (has_resized_since_last_swap_) {
    // Create a new CAContext for the new size. This allows new frame update at
    // the new size to be atomic with things like resizing the NSWindow.
    if (base::FeatureList::IsEnabled(features::kCATransactionV2) &&
        allow_remote_layers_) {
      params.ca_context_fence_mach_port.reset([ca_context_ createFencePort]);
      [ca_context_ setFencePort:params.ca_context_fence_mach_port.get()];
      ca_context_.layer = nil;
      ca_context_ = nil;
      root_ca_layer_ = nil;
      current_tree = nullptr;
    }
    has_resized_since_last_swap_ = false;
  }
#endif
  EnsureCAContextAndRootLayer();

  // Get the frame to be committed.
  auto& frame = presented_frames_.front();

  if (frame.layer_tree) {
    frame.layer_tree->CommitScheduledCALayers(
        root_ca_layer_, std::move(current_tree), pixel_size_, scale_factor_);
  } else {
    root_ca_layer_.sublayers = nil;
  }
  frame.has_committed = true;

  // Populate the CA layer parameters to send to the browser.
  // Send the swap parameters to the browser.
  if (frame.completion_callback) {
    TRACE_EVENT_INSTANT("test_gpu", "SwapBuffers", "GLImpl",
                        static_cast<int>(gl::GetGLImplementation()), "width",
                        pixel_size_.width());

    if (allow_remote_layers_) {
      params.ca_context_id = [ca_context_ contextId];
    } else {
      IOSurfaceRef io_surface = frame.layer_tree->GetContentIOSurface();
      if (io_surface) {
        DCHECK(!allow_remote_layers_);
        params.io_surface_mach_port.reset(IOSurfaceCreateMachPort(io_surface));
      }
    }
    params.pixel_size = pixel_size_;
    params.scale_factor = scale_factor_;

    // |frame.completion_callback| will reach this function:
    // SkiaOutputDeviceBufferQueue::DoFinishSwapBuffers().
    if (no_post_task_for_callback) {
      std::move(frame.completion_callback)
          .Run(gfx::SwapCompletionResult(gfx::SwapResult::SWAP_ACK,
                                         std::move(params)));
    } else {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(frame.completion_callback),
                         gfx::SwapCompletionResult(gfx::SwapResult::SWAP_ACK,
                                                   std::move(params))));
    }
  }

  gfx::PresentationFeedback feedback(base::TimeTicks::Now(), base::Hertz(60),
                                     /*flags=*/0);
  feedback.ca_layer_error_code = frame.ca_layer_error_code;

#if BUILDFLAG(IS_MAC)
  feedback.ready_timestamp = frame.ready_timestamp;
  feedback.latch_timestamp = base::TimeTicks::Now();
  feedback.interval = frame_interval;
  feedback.timestamp = display_time;

  // `update_vsync_params_callback` is not available in
  // SkiaOutputSurfaceImpl::BufferPresented(). Setting kVSync here will not
  // update vsync params.
  feedback.flags = gfx::PresentationFeedback::kHWCompletion |
                   gfx::PresentationFeedback::kVSync;
#endif

  // |frame.presentation_callback| will reach these functions:
  // viz::SkiaRenderer::DidReceiveReleasedOverlays(),
  // viz::Display::DidReceivePresentationFeedback(),
  // viz::SkiaOutputSurfaceImpl::BufferPresented().
  if (no_post_task_for_callback) {
    buffer_presented_callback_.Run(std::move(frame.presentation_callback),
                                   feedback);
  } else {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(buffer_presented_callback_,
                       std::move(frame.presentation_callback), feedback));
  }
}

void CALayerTreeCoordinator::SetMaxCALayerTrees(int max_ca_layer_trees) {
  presented_ca_layer_trees_max_length_ = max_ca_layer_trees;
}

int CALayerTreeCoordinator::NumPendingSwaps() const {
  int num = presented_frames_.size();
  if (num > 0 && presented_frames_.front().has_committed) {
    num--;
  }
  return num;
}

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

}  // namespace ui
