// 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.

#include "media/gpu/mac/video_toolbox_frame_converter.h"

#include <optional>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/task/bind_post_task.h"
#include "components/viz/common/resources/shared_image_format.h"
#include "gpu/GLES2/gl2extchromium.h"
#include "gpu/command_buffer/common/shared_image_usage.h"
#include "gpu/command_buffer/common/sync_token.h"
#include "gpu/command_buffer/service/scheduler.h"
#include "gpu/ipc/service/gpu_channel.h"
#include "gpu/ipc/service/gpu_channel_shared_image_interface.h"
#include "gpu/ipc/service/shared_image_stub.h"
#include "media/base/format_utils.h"
#include "media/base/mac/color_space_util_mac.h"
#include "media/base/mac/video_frame_mac.h"
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "media/base/video_types.h"
#include "media/gpu/mac/video_toolbox_decompression_metadata.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/gpu_memory_buffer_handle.h"
#include "ui/gfx/mac/io_surface.h"

namespace media {

namespace {

// The SharedImages created by this class to back VideoFrames can be read by the
// raster interface for canvas and by the GLES2 interface for WebGL in addition
// to being sent to the display compositor and/or used as overlays.
constexpr gpu::SharedImageUsageSet kSharedImageUsage =
    gpu::SHARED_IMAGE_USAGE_DISPLAY_READ | gpu::SHARED_IMAGE_USAGE_SCANOUT |
    gpu::SHARED_IMAGE_USAGE_MACOS_VIDEO_TOOLBOX |
    gpu::SHARED_IMAGE_USAGE_RASTER_READ | gpu::SHARED_IMAGE_USAGE_GLES2_READ;

constexpr char kSharedImageDebugLabel[] = "VideoToolboxVideoDecoder";

}  // namespace

VideoToolboxFrameConverter::VideoToolboxFrameConverter(
    scoped_refptr<base::SequencedTaskRunner> gpu_task_runner,
    std::unique_ptr<MediaLog> media_log,
    GetCommandBufferStubCB get_stub_cb)
    : base::RefCountedDeleteOnSequence<VideoToolboxFrameConverter>(
          gpu_task_runner),
      gpu_task_runner_(std::move(gpu_task_runner)),
      media_log_(std::move(media_log)),
      get_stub_cb_(std::move(get_stub_cb)) {
  DVLOG(1) << __func__;
  DCHECK(get_stub_cb_);
}

VideoToolboxFrameConverter::~VideoToolboxFrameConverter() {
  DVLOG(1) << __func__;
  DCHECK(gpu_task_runner_->RunsTasksInCurrentSequence());
  DestroyStub();
}

void VideoToolboxFrameConverter::OnWillDestroyStub(bool have_context) {
  DVLOG(1) << __func__;
  DCHECK(gpu_task_runner_->RunsTasksInCurrentSequence());
  DestroyStub();
}

void VideoToolboxFrameConverter::Initialize() {
  DVLOG(4) << __func__;
  DCHECK(gpu_task_runner_->RunsTasksInCurrentSequence());
  DCHECK(!initialized_);

  initialized_ = true;

  stub_ = std::move(get_stub_cb_).Run();
  if (!stub_) {
    MEDIA_LOG(ERROR, media_log_.get()) << "Failed to get command buffer stub";
    return;
  }

  DCHECK(stub_->channel()->task_runner()->BelongsToCurrentThread());

  stub_->AddDestructionObserver(this);

  // kHigh priority to work around crbug.com/1035750.
  wait_sequence_id_ = stub_->channel()->scheduler()->CreateSequence(
      gpu::SchedulingPriority::kHigh, stub_->channel()->task_runner());

  sis_ = stub_->channel()->shared_image_stub();
  if (!sis_) {
    MEDIA_LOG(ERROR, media_log_.get()) << "Failed to get shared image stub";
    DestroyStub();
    return;
  }
}

void VideoToolboxFrameConverter::DestroyStub() {
  DVLOG(4) << __func__;
  DCHECK(gpu_task_runner_->RunsTasksInCurrentSequence());

  sis_ = nullptr;

  if (stub_) {
    stub_->channel()->scheduler()->DestroySequence(wait_sequence_id_);
    stub_->RemoveDestructionObserver(this);
    stub_ = nullptr;
  }
}

void VideoToolboxFrameConverter::Convert(
    base::apple::ScopedCFTypeRef<CVImageBufferRef> image,
    std::unique_ptr<VideoToolboxDecodeMetadata> metadata,
    OutputCB output_cb) {
  DVLOG(3) << __func__;
  DCHECK(gpu_task_runner_->RunsTasksInCurrentSequence());

  if (!initialized_) {
    Initialize();
  }

  if (!stub_) {
    MEDIA_LOG(ERROR, media_log_.get()) << "Command buffer stub is missing";
    std::move(output_cb).Run(nullptr, std::move(metadata));
    return;
  }

  const gfx::Size coded_size(CVPixelBufferGetWidth(image.get()),
                             CVPixelBufferGetHeight(image.get()));
  const gfx::Rect visible_rect(CVImageBufferGetCleanRect(image.get()));
  const gfx::Size natural_size =
      metadata->aspect_ratio.GetNaturalSize(visible_rect);

  gfx::GpuMemoryBufferHandle handle(gfx::ScopedIOSurface(
      CVPixelBufferGetIOSurface(image.get()), base::scoped_policy::RETAIN));

  OSType pixel_format = IOSurfaceGetPixelFormat(handle.io_surface().get());
  std::optional<viz::SharedImageFormat> format =
      gfx::IOSurfacePixelFormatToSharedImageFormat(pixel_format);
  if (!format) {
    MEDIA_LOG(ERROR, media_log_.get())
        << "Failed to translate IOSurface format to SharedImageFormat "
        << pixel_format;
    std::move(output_cb).Run(nullptr, std::move(metadata));
    return;
  }

  std::optional<VideoPixelFormat> video_pixel_format =
      SharedImageFormatToVideoPixelFormat(*format);
  if (!video_pixel_format) {
    MEDIA_LOG(ERROR, media_log_.get())
        << "Failed to translate IOSurface format to VideoPixelFormat "
        << pixel_format;
    std::move(output_cb).Run(nullptr, std::move(metadata));
    return;
  }
  auto shared_image_interface = sis_->shared_image_interface();
  CHECK(shared_image_interface);

  // Extract IOSurface webgpu compatible attribute before image is moved.
  const bool is_webgpu_compatible =
      gfx::IOSurfacePixelFormatIsWebGPUCompatible(pixel_format);
  gpu::SharedImageUsageSet shared_image_usage = kSharedImageUsage;
  if (is_webgpu_compatible) {
    shared_image_usage |= gpu::SHARED_IMAGE_USAGE_WEBGPU_READ;
  }

  gfx::ColorSpace color_space = GetImageBufferColorSpace(image.get());
  if (!color_space.IsValid()) {
    // Chrome and macOS do not agree on the color space; force compositing to
    // ensure a consistent result. See crbug.com/343014700.
    shared_image_usage.RemoveAll(gpu::SHARED_IMAGE_USAGE_SCANOUT);
    // Always use limited range since we request a limited range output format.
    color_space = metadata->color_space.GetWithMatrixAndRange(
        metadata->color_space.GetMatrixID(), gfx::ColorSpace::RangeID::LIMITED);
  }

  auto shared_image = shared_image_interface->CreateSharedImage(
      {*format, coded_size, color_space, kTopLeft_GrSurfaceOrigin,
       kOpaque_SkAlphaType, shared_image_usage, kSharedImageDebugLabel},
      std::move(handle));
  if (!shared_image) {
    MEDIA_LOG(ERROR, media_log_.get()) << "Failed to create shared image";
    std::move(output_cb).Run(nullptr, std::move(metadata));
    return;
  }


  // |image| must be retained until after the release sync token passes.
  VideoFrame::ReleaseMailboxCB release_cb = base::BindPostTask(
      gpu_task_runner_,
      base::BindOnce(&VideoToolboxFrameConverter::OnVideoFrameReleased, this,
                     shared_image, std::move(image)));

  scoped_refptr<VideoFrame> frame = VideoFrame::WrapSharedImage(
      *video_pixel_format, shared_image, shared_image->creation_sync_token(),
      std::move(release_cb), visible_rect, natural_size, metadata->timestamp);

  if (!frame) {
    MEDIA_LOG(ERROR, media_log_.get()) << "Failed to create VideoFrame";
    std::move(output_cb).Run(nullptr, std::move(metadata));
    return;
  }

  frame->set_color_space(shared_image->color_space());
  frame->set_hdr_metadata(metadata->hdr_metadata);
  if (metadata->duration != kNoTimestamp && !metadata->duration.is_zero()) {
    frame->metadata().frame_duration = metadata->duration;
  }
  // Releasing |image| must happen after command buffer commands are complete
  // (not just submitted).
  frame->metadata().read_lock_fences_enabled = true;
  frame->metadata().is_webgpu_compatible =
      shared_image->usage().Has(gpu::SHARED_IMAGE_USAGE_WEBGPU_READ);

  // TODO(crbug.com/40227557): VideoToolbox can report software usage, should
  // we plumb that through?
  frame->metadata().power_efficient = true;

  std::move(output_cb).Run(std::move(frame), std::move(metadata));
}

void VideoToolboxFrameConverter::OnVideoFrameReleased(
    scoped_refptr<gpu::ClientSharedImage> client_shared_image,
    base::apple::ScopedCFTypeRef<CVImageBufferRef> image,
    const gpu::SyncToken& sync_token) {
  DVLOG(4) << __func__;
  DCHECK(gpu_task_runner_->RunsTasksInCurrentSequence());

  if (!stub_) {
    // Release |image| immediately.
    return;
  }

  if (client_shared_image) {
    client_shared_image->UpdateDestructionSyncToken(sync_token);
  }

  // Release |image|.
  stub_->channel()->scheduler()->ScheduleTask(gpu::Scheduler::Task(
      wait_sequence_id_, base::DoNothingWithBoundArgs(std::move(image)),
      std::vector<gpu::SyncToken>({sync_token})));
}

}  // namespace media
