// 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 "ash/components/arc/video_accelerator/gpu_arc_video_frame_pool.h"
#include <utility>
#include "ash/components/arc/video_accelerator/arc_video_accelerator_util.h"
#include "ash/components/arc/video_accelerator/protected_buffer_manager.h"
#include "base/functional/bind.h"
#include "base/posix/eintr_wrapper.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "gpu/ipc/common/gpu_memory_buffer_support.h"
#include "media/base/decoder_status.h"
#include "media/base/format_utils.h"
#include "media/base/video_types.h"
#include "media/gpu/buffer_validation.h"
#include "media/gpu/chromeos/native_pixmap_frame_resource.h"
#include "media/gpu/macros.h"
#include "media/media_buildflags.h"
#include "ui/gfx/buffer_format_util.h"
#if BUILDFLAG(USE_CHROMEOS_MEDIA_ACCELERATION)
#include "media/gpu/chromeos/platform_video_frame_utils.h"
#endif // BUILDFLAG(USE_CHROMEOS_MEDIA_ACCELERATION)
namespace arc {
namespace {
// Helper thunk called when all references to a video frame have been dropped.
// The thunk reschedules the OnFrameReleased callback on the correct task runner
// as frames can be destroyed on any thread. Note that the WeakPtr is wrapped in
// an std::optional, as a WeakPtr should only be dereferenced on the thread it
// was created on. If we don't wrap the WeakPtr the task runner will dereference
// the WeakPtr before calling this function causing an assert.
void OnFrameReleasedThunk(
std::optional<base::WeakPtr<GpuArcVideoFramePool>> weak_this,
base::SequencedTaskRunner* task_runner,
scoped_refptr<media::FrameResource> origin_frame) {
DCHECK(weak_this);
task_runner->PostTask(FROM_HERE,
base::BindOnce(&GpuArcVideoFramePool::OnFrameReleased,
*weak_this, std::move(origin_frame)));
}
// Get the video pixel format associated with the specified mojo pixel |format|.
media::VideoPixelFormat GetPixelFormat(mojom::HalPixelFormat format) {
switch (format) {
case mojom::HalPixelFormat::HAL_PIXEL_FORMAT_YV12:
return media::PIXEL_FORMAT_YV12;
case mojom::HalPixelFormat::HAL_PIXEL_FORMAT_NV12:
return media::PIXEL_FORMAT_NV12;
default:
return media::VideoPixelFormat::PIXEL_FORMAT_UNKNOWN;
}
}
} // namespace
GpuArcVideoFramePool::GpuArcVideoFramePool(
mojo::PendingAssociatedReceiver<mojom::VideoFramePool> video_frame_pool,
base::RepeatingClosure release_client_video_frames_cb,
scoped_refptr<ProtectedBufferManager> protected_buffer_manager)
: video_frame_pool_receiver_(this),
release_client_video_frames_cb_(
std::move(release_client_video_frames_cb)),
protected_buffer_manager_(std::move(protected_buffer_manager)) {
VLOGF(2);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
video_frame_pool_receiver_.Bind(std::move(video_frame_pool));
weak_this_ = weak_this_factory_.GetWeakPtr();
video_frame_pool_receiver_.set_disconnect_handler(
base::BindOnce(&GpuArcVideoFramePool::Stop, weak_this_));
client_task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault();
}
GpuArcVideoFramePool::~GpuArcVideoFramePool() {
VLOGF(2);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Invalidate all weak pointers to stop incoming callbacks.
weak_this_factory_.InvalidateWeakPtrs();
// The VdaVideoFramePool is blocked until this callback is executed, so we
// need to make sure to call it before destroying.
if (notify_layout_changed_cb_) {
std::move(notify_layout_changed_cb_)
.Run(media::CroStatus::Codes::kFailedToGetFrameLayout);
}
}
void GpuArcVideoFramePool::Initialize(
mojo::PendingAssociatedRemote<mojom::VideoFramePoolClient> client) {
VLOGF(2);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
return;
}
if (pool_client_) {
DVLOGF(3) << "Attempting to call GpuArcVideoFramePool::Initialize() when "
"it is already initialized";
return;
}
pool_client_version_ = client.version();
pool_client_.Bind(std::move(client));
pool_client_.set_disconnect_handler(
base::BindOnce(&GpuArcVideoFramePool::Stop, weak_this_));
}
void GpuArcVideoFramePool::AddVideoFrame(mojom::VideoFramePtr video_frame,
AddVideoFrameCallback callback) {
DVLOGF(3) << "id: " << video_frame->id;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
std::move(callback).Run(false);
return;
}
if (!pool_client_version_) {
DVLOGF(3) << "Unknown pool client version. Discarding video frame.";
std::move(callback).Run(false);
return;
}
if (!import_frame_cb_) {
// This can happen if the remote end calls AddVideoFrame() before we call
// RequestFrames() on it (a badly behaved remote end) or if due to
// unfortunate timing, AddVideoFrame() is received after having interrupted
// a request for frames because of a Reset(). In either case, we should
// ignore the frame.
//
// Note that we call |callback| with true. This is to prevent
// GpuVdContext::OnAddVideoFrameDone() on the libvda side from dispatching
// an error. This is consistent with how the legacy VDA stack handles the
// absence of an import callback: see
// VdVideoDecodeAccelerator::ImportBufferForPicture() which simply returns
// early without reporting an error to the client.
DVLOGF(3) << "Can't import the frame because there's no import callback";
std::move(callback).Run(true);
return;
}
// Discard frame because ACK from the client for the last RequestVideoFrames()
// has not been received yet.
if (awaiting_request_frames_ack_) {
CHECK_GE(pool_client_version_.value(), kMinVersionForRequestFramesAck);
DVLOGF(3) << "ACK from client not received after calling "
"Client::RequestVideoFrames(). "
<< "Discarding video frame.";
std::move(callback).Run(true);
return;
}
// Frames with the old coded size can still be added after resolution changes.
if (video_frame->coded_size != coded_size_) {
DVLOGF(3) << "Discarding video frame with old coded size";
std::move(callback).Run(true);
return;
}
media::VideoPixelFormat pixel_format = GetPixelFormat(video_frame->format);
if (pixel_format == media::VideoPixelFormat::PIXEL_FORMAT_UNKNOWN) {
VLOGF(1) << "Unsupported format: " << video_frame->format;
std::move(callback).Run(false);
return;
}
// Convert the Mojo buffer fd to a GpuMemoryBufferHandle.
base::ScopedFD fd = video_frame->handle_fd.TakeFD();
if (!fd.is_valid()) {
VLOGF(1) << "Invalid buffer handle";
std::move(callback).Run(false);
return;
}
gfx::GpuMemoryBufferHandle gmb_handle = CreateGpuMemoryHandle(
std::move(fd), video_frame->planes, pixel_format, video_frame->modifier);
if (gmb_handle.is_null()) {
VLOGF(1) << "Failed to create GPU memory handle from fd";
std::move(callback).Run(false);
return;
}
// If this is the first video frame added after requesting new video frames we
// may need to notify the VdaVideoFramePool that the layout changed.
if (notify_layout_changed_cb_) {
const uint64_t layout_modifier =
(gmb_handle.type == gfx::NATIVE_PIXMAP)
? gmb_handle.native_pixmap_handle.modifier
: gfx::NativePixmapHandle::kNoModifier;
std::vector<media::ColorPlaneLayout> color_planes;
for (const auto& plane : gmb_handle.native_pixmap_handle.planes) {
color_planes.emplace_back(plane.stride, plane.offset, plane.size);
}
auto fourcc = media::Fourcc::FromVideoPixelFormat(pixel_format);
if (!fourcc) {
VLOGF(1) << "Failed to convert to fourcc";
std::move(callback).Run(false);
return;
}
auto gb_layout = media::GpuBufferLayout::Create(
*fourcc, coded_size_, color_planes, layout_modifier);
if (!gb_layout) {
VLOGF(1) << "Failed to create GpuBufferLayout";
std::move(notify_layout_changed_cb_)
.Run(media::CroStatus::Codes::kFailedToChangeResolution);
std::move(callback).Run(false);
return;
}
std::move(notify_layout_changed_cb_).Run(*gb_layout);
}
scoped_refptr<media::FrameResource> origin_frame =
CreateFrame(std::move(gmb_handle), pixel_format);
if (!origin_frame) {
VLOGF(1) << "Failed to create frame from fd";
std::move(callback).Run(false);
return;
}
// This passes because GetFrameStorageType() is hard coded to match
// the storage type of frames produced by CreateFrame().
CHECK_EQ(origin_frame->storage_type(), GetFrameStorageType());
auto it = buffer_id_to_video_frame_id_.emplace(
origin_frame->GetSharedMemoryId(), video_frame->id);
DCHECK(it.second);
// Wrap the video frame and attach a destruction observer so we're notified
// when all references to the frame have been dropped.
scoped_refptr<media::FrameResource> wrapped_frame =
origin_frame->CreateWrappingFrame();
wrapped_frame->AddDestructionObserver(base::BindOnce(
&OnFrameReleasedThunk, weak_this_, base::RetainedRef(client_task_runner_),
std::move(origin_frame)));
// Add the frame to the underlying frame pool.
import_frame_cb_.Run(std::move(wrapped_frame));
std::move(callback).Run(true);
}
void GpuArcVideoFramePool::WillResetDecoder() {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
return;
}
// GpuArcVideoDecoder should ensure WillResetDecoder() is not called while a
// reset is in progress.
CHECK(!decoder_is_resetting_);
decoder_is_resetting_ = true;
if (notify_layout_changed_cb_) {
// If we're here, a Reset() has occurred in the the middle of a request for
// frames. That means that the VdaVideoFramePool is blocked waiting for a
// call to |notify_layout_changed_cb_|. Here, we unblock it by calling
// |notify_layout_changed_cb_| with kResetRequired thus aborting the request
// for frames.
std::move(notify_layout_changed_cb_)
.Run(media::CroStatus::Codes::kResetRequired);
import_frame_cb_.Reset();
}
}
void GpuArcVideoFramePool::OnDecoderResetDone() {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
return;
}
decoder_is_resetting_ = false;
}
void GpuArcVideoFramePool::RequestFrames(
const media::Fourcc& fourcc,
const gfx::Size& coded_size,
const gfx::Rect& visible_rect,
size_t max_num_frames,
NotifyLayoutChangedCb notify_layout_changed_cb,
ImportFrameCb import_frame_cb) {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!notify_layout_changed_cb_);
// Notify the GpuArcVideoDecoder that we're no longer tracking frames
// previously added to the pool.
release_client_video_frames_cb_.Run();
if (has_error_ || !pool_client_version_) {
std::move(notify_layout_changed_cb)
.Run(media::CroStatus::Codes::kFailedToGetFrameLayout);
return;
}
notify_layout_changed_cb_ = std::move(notify_layout_changed_cb);
import_frame_cb_ = std::move(import_frame_cb);
if (decoder_is_resetting_) {
// If we're here, it means that due to unfortunate timing, a Reset() request
// was received before this RequestFrames(). The VdaVideoFramePool is
// blocked waiting on a call to |notify_layout_changed_cb_|. Therefore, we
// call it here with kResetRequired thus aborting the request for frames.
std::move(notify_layout_changed_cb_)
.Run(media::CroStatus::Codes::kResetRequired);
import_frame_cb_.Reset();
return;
}
pending_frame_requests_.push(
base::BindOnce(&GpuArcVideoFramePool::HandleRequestFrames, weak_this_,
fourcc, coded_size, visible_rect, max_num_frames));
CallPendingRequestFrames();
}
void GpuArcVideoFramePool::HandleRequestFrames(const media::Fourcc& fourcc,
const gfx::Size& coded_size,
const gfx::Rect& visible_rect,
size_t max_num_frames) {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Pending calls to HandleRequestFrames() are cleared when an error is
// detected (see logic in Stop()) and new ones are not scheduled after that
// (see logic in RequestFrames()).
CHECK(!has_error_);
// Calls to HandleRequestFrames() are scheduled only if the client version is
// known (see the logic in RequestFrames()). If the |pool_client_version_|
// becomes nullopt (see Stop()), pending calls are dropped.
CHECK(pool_client_version_);
// HandleRequestFrames() should only be called if we're not waiting for an ACK
// for a previous request for frames.
CHECK_GE(pool_client_version_.value(), kMinVersionForRequestFramesAck);
CHECK(!awaiting_request_frames_ack_);
awaiting_request_frames_ack_ = true;
coded_size_ = coded_size;
pool_client_->RequestVideoFrames(
/*format=*/fourcc.ToVideoPixelFormat(), coded_size, visible_rect,
max_num_frames,
base::BindOnce(&GpuArcVideoFramePool::OnRequestVideoFramesDone,
weak_this_));
}
void GpuArcVideoFramePool::CallPendingRequestFrames() {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!has_error_);
if (awaiting_request_frames_ack_ || pending_frame_requests_.empty()) {
// Either we're still waiting for an ACK for a previous call to
// |pool_client_|->RequestVideoFrames() or there are no more RequestFrames()
// to be submitted to the remote end.
return;
}
std::move(pending_frame_requests_.front()).Run();
pending_frame_requests_.pop();
}
void GpuArcVideoFramePool::OnRequestVideoFramesDone() {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
return;
}
CHECK(pool_client_version_.has_value());
CHECK_GE(pool_client_version_.value(), kMinVersionForRequestFramesAck);
CHECK(awaiting_request_frames_ack_);
awaiting_request_frames_ack_ = false;
// Continue any queued requests for video frames that were made while waiting
// for the current request for video frames to complete.
CallPendingRequestFrames();
}
media::VideoFrame::StorageType GpuArcVideoFramePool::GetFrameStorageType()
const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This is validated at runtime to be in sync with the frame storage type.
return media::VideoFrame::STORAGE_DMABUFS;
}
std::optional<int32_t> GpuArcVideoFramePool::GetVideoFrameId(
const media::VideoFrame* video_frame) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(video_frame);
if (has_error_) {
return std::nullopt;
}
auto it =
buffer_id_to_video_frame_id_.find(media::GetSharedMemoryId(*video_frame));
return it != buffer_id_to_video_frame_id_.end()
? std::optional<int32_t>(it->second)
: std::nullopt;
}
void GpuArcVideoFramePool::OnFrameReleased(
scoped_refptr<media::FrameResource> origin_frame) {
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
return;
}
auto it =
buffer_id_to_video_frame_id_.find(origin_frame->GetSharedMemoryId());
DCHECK(it != buffer_id_to_video_frame_id_.end());
buffer_id_to_video_frame_id_.erase(it);
}
gfx::GpuMemoryBufferHandle GpuArcVideoFramePool::CreateGpuMemoryHandle(
base::ScopedFD fd,
const std::vector<VideoFramePlane>& planes,
media::VideoPixelFormat pixel_format,
uint64_t modifier) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!has_error_);
// Check whether we need to use protected buffers.
if (!secure_mode_.has_value()) {
base::ScopedFD dup_fd(HANDLE_EINTR(dup(fd.get())));
secure_mode_ = protected_buffer_manager_ &&
protected_buffer_manager_->IsProtectedNativePixmapHandle(
std::move(dup_fd));
}
gfx::GpuMemoryBufferHandle gmb_handle;
if (*secure_mode_) {
// Get the protected buffer associated with the |fd|.
gfx::NativePixmapHandle protected_native_pixmap =
protected_buffer_manager_->GetProtectedNativePixmapHandleFor(
std::move(fd));
if (protected_native_pixmap.planes.size() == 0) {
VLOGF(1) << "No protected native pixmap found for handle";
return gfx::GpuMemoryBufferHandle();
}
gmb_handle.type = gfx::NATIVE_PIXMAP;
gmb_handle.native_pixmap_handle = std::move(protected_native_pixmap);
// Explicitly verify the GPU Memory Buffer Handle here. Note that we do not
// do this for non-protected content because the verification happens on
// creation in that path.
if (!media::VerifyGpuMemoryBufferHandle(pixel_format, coded_size_,
gmb_handle)) {
VLOGF(1) << "Invalid GpuMemoryBufferHandle for protected content";
return gfx::GpuMemoryBufferHandle();
}
} else {
std::vector<base::ScopedFD> fds = DuplicateFD(std::move(fd), planes.size());
if (fds.empty()) {
VLOGF(1) << "Failed to duplicate fd";
return gfx::GpuMemoryBufferHandle();
}
// Verification of the GPU Memory Buffer Handle is handled under the hood in
// this call.
auto handle = CreateGpuMemoryBufferHandle(
pixel_format, modifier, coded_size_, std::move(fds), planes);
if (!handle) {
VLOGF(1) << "Failed to create GpuMemoryBufferHandle";
return gfx::GpuMemoryBufferHandle();
}
gmb_handle = std::move(handle).value();
}
gmb_handle.id = media::GetNextGpuMemoryBufferId();
return gmb_handle;
}
scoped_refptr<media::FrameResource> GpuArcVideoFramePool::CreateFrame(
gfx::GpuMemoryBufferHandle gmb_handle,
media::VideoPixelFormat pixel_format) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!has_error_);
auto buffer_format = media::VideoPixelFormatToGfxBufferFormat(pixel_format);
CHECK(buffer_format);
// Usage is SCANOUT_CPU_READ_WRITE because we may need to map the buffer in
// order to use the LibYUVImageProcessorBackend.
return media::NativePixmapFrameResource::Create(
gfx::Rect(coded_size_), coded_size_, base::TimeDelta(),
gfx::BufferUsage::SCANOUT_CPU_READ_WRITE,
base::MakeRefCounted<gfx::NativePixmapDmaBuf>(
coded_size_, *buffer_format,
std::move(gmb_handle.native_pixmap_handle)));
}
void GpuArcVideoFramePool::Stop() {
VLOGF(2);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (has_error_) {
return;
}
has_error_ = true;
weak_this_factory_.InvalidateWeakPtrs();
pool_client_version_.reset();
pending_frame_requests_ = {};
if (notify_layout_changed_cb_) {
std::move(notify_layout_changed_cb_)
.Run(media::CroStatus::Codes::kFailedToGetFrameLayout);
}
}
} // namespace arc