blob: 42c1c3b5cd2f743f6c7c1f8f475032708c12e012 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/renderers/paint_canvas_video_renderer.h"
#include <GLES3/gl3.h>
#include <limits>
#include "base/barrier_closure.h"
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/numerics/checked_math.h"
#include "base/synchronization/waitable_event.h"
#include "base/system/sys_info.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_image_builder.h"
#include "components/viz/common/gpu/raster_context_provider.h"
#include "components/viz/common/resources/resource_format.h"
#include "gpu/GLES2/gl2extchromium.h"
#include "gpu/command_buffer/client/context_support.h"
#include "gpu/command_buffer/client/gles2_interface.h"
#include "gpu/command_buffer/client/raster_interface.h"
#include "gpu/command_buffer/client/shared_image_interface.h"
#include "gpu/command_buffer/common/capabilities.h"
#include "gpu/command_buffer/common/mailbox_holder.h"
#include "gpu/command_buffer/common/shared_image_usage.h"
#include "media/base/data_buffer.h"
#include "media/base/video_frame.h"
#include "media/base/wait_and_replace_sync_token_client.h"
#include "third_party/libyuv/include/libyuv.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkImageGenerator.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/gpu/GrBackendSurface.h"
#include "third_party/skia/include/gpu/GrDirectContext.h"
#include "third_party/skia/include/gpu/gl/GrGLTypes.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/skia_util.h"
// Skia internal format depends on a platform. On Android it is ABGR, on others
// it's ARGB. YUV_MATRIX(), YUV_ORDER() conditionally remap YUV to YVU for ABGR.
#if SK_B32_SHIFT == 0 && SK_G32_SHIFT == 8 && SK_R32_SHIFT == 16 && \
SK_A32_SHIFT == 24
#define OUTPUT_ARGB 1
#define LIBYUV_ABGR_TO_ARGB libyuv::ABGRToARGB
#define YUV_MATRIX(matrix) matrix
#define YUV_ORDER(y, y_stride, u, u_stride, v, v_stride) \
(y), (y_stride), (u), (u_stride), (v), (v_stride)
#define GBR_TO_RGB_ORDER(y, y_stride, u, u_stride, v, v_stride) \
(v), (v_stride), (y), (y_stride), (u), (u_stride)
#define LIBYUV_NV12_TO_ARGB_MATRIX libyuv::NV12ToARGBMatrix
#elif SK_R32_SHIFT == 0 && SK_G32_SHIFT == 8 && SK_B32_SHIFT == 16 && \
SK_A32_SHIFT == 24
#define OUTPUT_ARGB 0
#define LIBYUV_ABGR_TO_ARGB libyuv::ARGBToABGR
#define YUV_MATRIX(matrix) matrix##VU
#define YUV_ORDER(y, y_stride, u, u_stride, v, v_stride) \
(y), (y_stride), (v), (v_stride), (u), (u_stride)
#define GBR_TO_RGB_ORDER(y, y_stride, u, u_stride, v, v_stride) \
(u), (u_stride), (y), (y_stride), (v), (v_stride)
#define LIBYUV_NV12_TO_ARGB_MATRIX libyuv::NV21ToARGBMatrix
#else
#error Unexpected Skia ARGB_8888 layout!
#endif
namespace media {
namespace {
// This class keeps the last image drawn.
// We delete the temporary resource if it is not used for 3 seconds.
const int kTemporaryResourceDeletionDelay = 3; // Seconds;
// Helper class that begins/ends access to a mailbox within a scope. The mailbox
// must have been imported into |texture|.
class ScopedSharedImageAccess {
public:
ScopedSharedImageAccess(
gpu::gles2::GLES2Interface* gl,
GLuint texture,
const gpu::Mailbox& mailbox,
GLenum access = GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM)
: gl(gl),
ri(nullptr),
texture(texture),
is_shared_image(mailbox.IsSharedImage()) {
if (is_shared_image)
gl->BeginSharedImageAccessDirectCHROMIUM(texture, access);
}
// TODO(crbug.com/1023270): Remove this ctor once we're no longer relying on
// texture ids for Mailbox access as that is only supported on
// RasterImplementationGLES.
ScopedSharedImageAccess(
gpu::raster::RasterInterface* ri,
GLuint texture,
const gpu::Mailbox& mailbox,
GLenum access = GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM)
: gl(nullptr),
ri(ri),
texture(texture),
is_shared_image(mailbox.IsSharedImage()) {
if (is_shared_image)
ri->BeginSharedImageAccessDirectCHROMIUM(texture, access);
}
~ScopedSharedImageAccess() {
if (is_shared_image) {
if (gl)
gl->EndSharedImageAccessDirectCHROMIUM(texture);
else
ri->EndSharedImageAccessDirectCHROMIUM(texture);
}
}
private:
gpu::gles2::GLES2Interface* gl;
gpu::raster::RasterInterface* ri;
GLuint texture;
bool is_shared_image;
};
// Waits for a sync token and import the mailbox as texture.
GLuint SynchronizeAndImportMailbox(gpu::gles2::GLES2Interface* gl,
const gpu::SyncToken& sync_token,
const gpu::Mailbox& mailbox) {
gl->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
return mailbox.IsSharedImage()
? gl->CreateAndTexStorage2DSharedImageCHROMIUM(mailbox.name)
: gl->CreateAndConsumeTextureCHROMIUM(mailbox.name);
}
const gpu::MailboxHolder& GetVideoFrameMailboxHolder(VideoFrame* video_frame) {
DCHECK(video_frame->HasTextures());
DCHECK_EQ(video_frame->NumTextures(), 1u);
DCHECK(PIXEL_FORMAT_ARGB == video_frame->format() ||
PIXEL_FORMAT_XRGB == video_frame->format() ||
PIXEL_FORMAT_RGB24 == video_frame->format() ||
PIXEL_FORMAT_ABGR == video_frame->format() ||
PIXEL_FORMAT_XB30 == video_frame->format() ||
PIXEL_FORMAT_XR30 == video_frame->format() ||
PIXEL_FORMAT_NV12 == video_frame->format())
<< "Format: " << VideoPixelFormatToString(video_frame->format());
const gpu::MailboxHolder& mailbox_holder = video_frame->mailbox_holder(0);
DCHECK(mailbox_holder.texture_target == GL_TEXTURE_2D ||
mailbox_holder.texture_target == GL_TEXTURE_RECTANGLE_ARB ||
mailbox_holder.texture_target == GL_TEXTURE_EXTERNAL_OES)
<< mailbox_holder.texture_target;
return mailbox_holder;
}
// Imports a VideoFrame that contains a single mailbox into a newly created GL
// texture, after synchronization with the sync token. Returns the GL texture.
// |mailbox| is set to the imported mailbox.
GLuint ImportVideoFrameSingleMailbox(gpu::gles2::GLES2Interface* gl,
VideoFrame* video_frame,
gpu::Mailbox* mailbox) {
const gpu::MailboxHolder& mailbox_holder =
GetVideoFrameMailboxHolder(video_frame);
*mailbox = mailbox_holder.mailbox;
return SynchronizeAndImportMailbox(gl, mailbox_holder.sync_token, *mailbox);
}
gpu::Mailbox SynchronizeVideoFrameSingleMailbox(
gpu::raster::RasterInterface* ri,
VideoFrame* video_frame) {
const gpu::MailboxHolder& mailbox_holder =
GetVideoFrameMailboxHolder(video_frame);
ri->WaitSyncTokenCHROMIUM(mailbox_holder.sync_token.GetConstData());
return mailbox_holder.mailbox;
}
// Wraps a GL RGBA texture into a SkImage.
sk_sp<SkImage> WrapGLTexture(
GLenum target,
GLuint texture_id,
const gfx::Size& size,
viz::RasterContextProvider* raster_context_provider) {
GrGLTextureInfo texture_info;
texture_info.fID = texture_id;
texture_info.fTarget = target;
// TODO(bsalomon): GrGLTextureInfo::fFormat and SkColorType passed to
// SkImage factory should reflect video_frame->format(). Update once
// Skia supports GL_RGB. skbug.com/7533
texture_info.fFormat = GL_RGBA8_OES;
GrBackendTexture backend_texture(size.width(), size.height(),
GrMipMapped::kNo, texture_info);
return SkImage::MakeFromAdoptedTexture(
raster_context_provider->GrContext(), backend_texture,
kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
}
void VideoFrameCopyTextureOrSubTexture(gpu::gles2::GLES2Interface* gl,
const gfx::Size& coded_size,
const gfx::Rect& visible_rect,
GLuint source_texture,
unsigned int target,
unsigned int texture,
unsigned int internal_format,
unsigned int format,
unsigned int type,
int level,
bool premultiply_alpha,
bool flip_y) {
// The video is stored in a unmultiplied format, so premultiply if necessary.
// Application itself needs to take care of setting the right |flip_y|
// value down to get the expected result.
// "flip_y == true" means to reverse the video orientation while
// "flip_y == false" means to keep the intrinsic orientation.
if (visible_rect != gfx::Rect(coded_size)) {
// Must reallocate the destination texture and copy only a sub-portion.
// There should always be enough data in the source texture to
// cover this copy.
DCHECK_LE(visible_rect.width(), coded_size.width());
DCHECK_LE(visible_rect.height(), coded_size.height());
gl->BindTexture(target, texture);
gl->TexImage2D(target, level, internal_format, visible_rect.width(),
visible_rect.height(), 0, format, type, nullptr);
gl->CopySubTextureCHROMIUM(source_texture, 0, target, texture, level, 0, 0,
visible_rect.x(), visible_rect.y(),
visible_rect.width(), visible_rect.height(),
flip_y, premultiply_alpha, false);
} else {
gl->CopyTextureCHROMIUM(source_texture, 0, target, texture, level,
internal_format, type, flip_y, premultiply_alpha,
false);
}
}
void OnQueryDone(scoped_refptr<VideoFrame> video_frame,
gpu::raster::RasterInterface* ri,
unsigned query_id) {
ri->DeleteQueriesEXT(1, &query_id);
// |video_frame| is dropped here.
}
void SynchronizeVideoFrameRead(scoped_refptr<VideoFrame> video_frame,
gpu::raster::RasterInterface* ri,
gpu::ContextSupport* context_support) {
DCHECK(ri);
WaitAndReplaceSyncTokenClient client(ri);
video_frame->UpdateReleaseSyncToken(&client);
if (video_frame->metadata().read_lock_fences_enabled) {
// |video_frame| must be kept alive during read operations.
DCHECK(context_support);
unsigned query_id = 0;
ri->GenQueriesEXT(1, &query_id);
DCHECK(query_id);
ri->BeginQueryEXT(GL_COMMANDS_COMPLETED_CHROMIUM, query_id);
ri->EndQueryEXT(GL_COMMANDS_COMPLETED_CHROMIUM);
context_support->SignalQuery(
query_id, base::BindOnce(&OnQueryDone, video_frame, ri, query_id));
}
}
// TODO(thomasanderson): Remove these and use std::gcd and std::lcm once we're
// building with C++17.
size_t GCD(size_t a, size_t b) {
return a == 0 ? b : GCD(b % a, a);
}
size_t LCM(size_t a, size_t b) {
return a / GCD(a, b) * b;
}
const libyuv::YuvConstants* GetYuvContantsForColorSpace(SkYUVColorSpace cs) {
switch (cs) {
case kJPEG_Full_SkYUVColorSpace:
return &YUV_MATRIX(libyuv::kYuvJPEGConstants);
case kRec601_Limited_SkYUVColorSpace:
return &YUV_MATRIX(libyuv::kYuvI601Constants);
case kRec709_Full_SkYUVColorSpace:
return &YUV_MATRIX(libyuv::kYuvF709Constants);
case kRec709_Limited_SkYUVColorSpace:
return &YUV_MATRIX(libyuv::kYuvH709Constants);
case kBT2020_8bit_Full_SkYUVColorSpace:
case kBT2020_10bit_Full_SkYUVColorSpace:
case kBT2020_12bit_Full_SkYUVColorSpace:
return &YUV_MATRIX(libyuv::kYuvV2020Constants);
case kBT2020_8bit_Limited_SkYUVColorSpace:
case kBT2020_10bit_Limited_SkYUVColorSpace:
case kBT2020_12bit_Limited_SkYUVColorSpace:
return &YUV_MATRIX(libyuv::kYuv2020Constants);
case kIdentity_SkYUVColorSpace:
NOTREACHED();
return nullptr;
};
}
void ConvertVideoFrameToRGBPixelsTask(const VideoFrame* video_frame,
void* rgb_pixels,
size_t row_bytes,
bool premultiply_alpha,
size_t task_index,
size_t n_tasks,
base::RepeatingClosure* done) {
const VideoPixelFormat format = video_frame->format();
const int width = video_frame->visible_rect().width();
const int height = video_frame->visible_rect().height();
size_t rows_per_chunk = 1;
for (size_t plane = 0; plane < VideoFrame::kMaxPlanes; ++plane) {
if (VideoFrame::IsValidPlane(format, plane)) {
rows_per_chunk =
LCM(rows_per_chunk, VideoFrame::SampleSize(format, plane).height());
}
}
base::CheckedNumeric<size_t> chunks = height / rows_per_chunk;
const size_t chunk_start = (chunks * task_index / n_tasks).ValueOrDie();
const size_t chunk_end = (chunks * (task_index + 1) / n_tasks).ValueOrDie();
// Indivisible heights must process any remaining rows in the last task.
size_t rows = (chunk_end - chunk_start) * rows_per_chunk;
if (task_index + 1 == n_tasks)
rows += height % rows_per_chunk;
struct {
int stride;
const uint8_t* data;
} plane_meta[VideoFrame::kMaxPlanes];
for (size_t plane = 0; plane < VideoFrame::kMaxPlanes; ++plane) {
if (VideoFrame::IsValidPlane(format, plane)) {
plane_meta[plane] = {
// Note: Unlike |data|, stride does not need to be adjusted by the
// visible rect and sample size. Adding the full frame stride to a
// pixel on row N and column M will wrap to column M on row N + 1.
.stride = video_frame->stride(plane),
.data = video_frame->visible_data(plane) +
video_frame->stride(plane) * (chunk_start * rows_per_chunk) /
VideoFrame::SampleSize(format, plane).height()};
} else {
plane_meta[plane] = {.stride = 0, .data = nullptr};
}
}
uint8_t* pixels = static_cast<uint8_t*>(rgb_pixels) +
row_bytes * chunk_start * rows_per_chunk;
if (format == PIXEL_FORMAT_ARGB || format == PIXEL_FORMAT_XRGB ||
format == PIXEL_FORMAT_ABGR || format == PIXEL_FORMAT_XBGR) {
DCHECK_LE(width, static_cast<int>(row_bytes));
const uint8_t* data = plane_meta[VideoFrame::kARGBPlane].data;
if ((OUTPUT_ARGB &&
(format == PIXEL_FORMAT_ARGB || format == PIXEL_FORMAT_XRGB)) ||
(!OUTPUT_ARGB &&
(format == PIXEL_FORMAT_ABGR || format == PIXEL_FORMAT_XBGR))) {
for (size_t i = 0; i < rows; i++) {
memcpy(pixels, data, width * 4);
pixels += row_bytes;
data += plane_meta[VideoFrame::kARGBPlane].stride;
}
} else {
LIBYUV_ABGR_TO_ARGB(plane_meta[VideoFrame::kARGBPlane].data,
plane_meta[VideoFrame::kARGBPlane].stride, pixels,
row_bytes, width, rows);
}
done->Run();
return;
}
// TODO(crbug.com/828599): This should default to BT.709 color space.
auto yuv_cs = kRec601_SkYUVColorSpace;
video_frame->ColorSpace().ToSkYUVColorSpace(video_frame->BitDepth(), &yuv_cs);
const libyuv::YuvConstants* matrix = GetYuvContantsForColorSpace(yuv_cs);
if (!video_frame->data(VideoFrame::kUPlane) &&
!video_frame->data(VideoFrame::kVPlane)) {
DCHECK_EQ(format, PIXEL_FORMAT_I420);
// For monochrome content ARGB and ABGR are interchangeable.
libyuv::I400ToARGBMatrix(plane_meta[VideoFrame::kYPlane].data,
plane_meta[VideoFrame::kYPlane].stride, pixels,
row_bytes, matrix, width, rows);
done->Run();
return;
}
auto convert_yuv = [&](const libyuv::YuvConstants* matrix, auto&& func) {
func(YUV_ORDER(plane_meta[VideoFrame::kYPlane].data,
plane_meta[VideoFrame::kYPlane].stride,
plane_meta[VideoFrame::kUPlane].data,
plane_meta[VideoFrame::kUPlane].stride,
plane_meta[VideoFrame::kVPlane].data,
plane_meta[VideoFrame::kVPlane].stride),
pixels, row_bytes, matrix, width, rows);
};
auto convert_yuv16 = [&](const libyuv::YuvConstants* matrix, auto&& func) {
func(YUV_ORDER(reinterpret_cast<const uint16_t*>(
plane_meta[VideoFrame::kYPlane].data),
plane_meta[VideoFrame::kYPlane].stride / 2,
reinterpret_cast<const uint16_t*>(
plane_meta[VideoFrame::kUPlane].data),
plane_meta[VideoFrame::kUPlane].stride / 2,
reinterpret_cast<const uint16_t*>(
plane_meta[VideoFrame::kVPlane].data),
plane_meta[VideoFrame::kVPlane].stride / 2),
pixels, row_bytes, matrix, width, rows);
};
switch (format) {
case PIXEL_FORMAT_YV12:
case PIXEL_FORMAT_I420:
convert_yuv(matrix, libyuv::I420ToARGBMatrix);
break;
case PIXEL_FORMAT_I422:
convert_yuv(matrix, libyuv::I422ToARGBMatrix);
break;
case PIXEL_FORMAT_I444:
// Special case for 4:4:4 RGB encoded videos.
if (video_frame->ColorSpace().GetMatrixID() ==
gfx::ColorSpace::MatrixID::GBR) {
libyuv::MergeARGBPlane(
GBR_TO_RGB_ORDER(plane_meta[VideoFrame::kYPlane].data,
plane_meta[VideoFrame::kYPlane].stride,
plane_meta[VideoFrame::kUPlane].data,
plane_meta[VideoFrame::kUPlane].stride,
plane_meta[VideoFrame::kVPlane].data,
plane_meta[VideoFrame::kVPlane].stride),
plane_meta[VideoFrame::kAPlane].data,
plane_meta[VideoFrame::kAPlane].stride, pixels, row_bytes, width,
rows);
} else {
convert_yuv(matrix, libyuv::I444ToARGBMatrix);
}
break;
case PIXEL_FORMAT_YUV420P10:
convert_yuv16(matrix, libyuv::I010ToARGBMatrix);
break;
case PIXEL_FORMAT_YUV422P10:
convert_yuv16(matrix, libyuv::I210ToARGBMatrix);
break;
case PIXEL_FORMAT_I420A:
libyuv::I420AlphaToARGBMatrix(
YUV_ORDER(plane_meta[VideoFrame::kYPlane].data,
plane_meta[VideoFrame::kYPlane].stride,
plane_meta[VideoFrame::kUPlane].data,
plane_meta[VideoFrame::kUPlane].stride,
plane_meta[VideoFrame::kVPlane].data,
plane_meta[VideoFrame::kVPlane].stride),
plane_meta[VideoFrame::kAPlane].data,
plane_meta[VideoFrame::kAPlane].stride, pixels, row_bytes, matrix,
width, rows, premultiply_alpha);
break;
case PIXEL_FORMAT_NV12:
LIBYUV_NV12_TO_ARGB_MATRIX(plane_meta[VideoFrame::kYPlane].data,
plane_meta[VideoFrame::kYPlane].stride,
plane_meta[VideoFrame::kUVPlane].data,
plane_meta[VideoFrame::kUVPlane].stride,
pixels, row_bytes, matrix, width, rows);
break;
case PIXEL_FORMAT_YUV420P9:
case PIXEL_FORMAT_YUV422P9:
case PIXEL_FORMAT_YUV444P9:
case PIXEL_FORMAT_YUV444P10:
case PIXEL_FORMAT_YUV420P12:
case PIXEL_FORMAT_YUV422P12:
case PIXEL_FORMAT_YUV444P12:
case PIXEL_FORMAT_Y16:
NOTREACHED()
<< "These cases should be handled in ConvertVideoFrameToRGBPixels";
break;
case PIXEL_FORMAT_UYVY:
case PIXEL_FORMAT_NV21:
case PIXEL_FORMAT_YUY2:
case PIXEL_FORMAT_ARGB:
case PIXEL_FORMAT_BGRA:
case PIXEL_FORMAT_XRGB:
case PIXEL_FORMAT_RGB24:
case PIXEL_FORMAT_MJPEG:
case PIXEL_FORMAT_ABGR:
case PIXEL_FORMAT_XBGR:
case PIXEL_FORMAT_P016LE:
case PIXEL_FORMAT_XR30:
case PIXEL_FORMAT_XB30:
case PIXEL_FORMAT_RGBAF16:
case PIXEL_FORMAT_UNKNOWN:
NOTREACHED() << "Only YUV formats and Y16 are supported, got: "
<< media::VideoPixelFormatToString(format);
}
done->Run();
}
// Valid gl texture internal format that can try to use direct uploading path.
bool ValidFormatForDirectUploading(
viz::RasterContextProvider* raster_context_provider,
GrGLenum format,
unsigned int type) {
const gpu::Capabilities& context_caps =
raster_context_provider->ContextCapabilities();
switch (format) {
case GL_RGBA:
return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_SHORT_4_4_4_4;
case GL_RGB:
return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_SHORT_5_6_5;
// WebGL2 supported sized formats
case GL_RGBA8:
case GL_RGB565:
case GL_RGBA16F:
case GL_RGB8:
case GL_RGB10_A2:
case GL_RGBA4:
return context_caps.major_version >= 3;
default:
return false;
}
}
std::tuple<SkYUVAInfo::PlaneConfig, SkYUVAInfo::Subsampling>
VideoPixelFormatAsSkYUVAInfoValues(VideoPixelFormat format) {
// The 9, 10, and 12 bit formats could be added here if GetYUVAPlanes() were
// updated to convert data to unorm16/float16. Similarly, alpha planes and
// formats with interleaved planes (e.g. NV12) could be supported if that
// function were updated to not assume 3 separate Y, U, and V planes. Also,
// GpuImageDecodeCache would need be able to handle plane configurations
// other than 3 separate y, u, and v planes (crbug.com/910276).
switch (format) {
case PIXEL_FORMAT_I420:
return {SkYUVAInfo::PlaneConfig::kY_U_V, SkYUVAInfo::Subsampling::k420};
case PIXEL_FORMAT_I422:
return {SkYUVAInfo::PlaneConfig::kY_U_V, SkYUVAInfo::Subsampling::k422};
case PIXEL_FORMAT_I444:
return {SkYUVAInfo::PlaneConfig::kY_U_V, SkYUVAInfo::Subsampling::k444};
default:
return {SkYUVAInfo::PlaneConfig::kUnknown,
SkYUVAInfo::Subsampling::kUnknown};
}
}
} // anonymous namespace
// Generates an RGB image from a VideoFrame. Convert YUV to RGB plain on GPU.
class VideoImageGenerator : public cc::PaintImageGenerator {
public:
VideoImageGenerator(scoped_refptr<VideoFrame> frame)
: cc::PaintImageGenerator(
SkImageInfo::MakeN32Premul(frame->visible_rect().width(),
frame->visible_rect().height())),
frame_(std::move(frame)) {
DCHECK(!frame_->HasTextures());
}
~VideoImageGenerator() override = default;
sk_sp<SkData> GetEncodedData() const override { return nullptr; }
bool GetPixels(const SkImageInfo& info,
void* pixels,
size_t row_bytes,
size_t frame_index,
cc::PaintImage::GeneratorClientId client_id,
uint32_t lazy_pixel_ref) override {
DCHECK_EQ(frame_index, 0u);
// If skia couldn't do the YUV conversion on GPU, we will on CPU.
PaintCanvasVideoRenderer::ConvertVideoFrameToRGBPixels(frame_.get(), pixels,
row_bytes);
return true;
}
bool QueryYUVA(const SkYUVAPixmapInfo::SupportedDataTypes&,
SkYUVAPixmapInfo* info) const override {
SkYUVAInfo::PlaneConfig plane_config;
SkYUVAInfo::Subsampling subsampling;
std::tie(plane_config, subsampling) =
VideoPixelFormatAsSkYUVAInfoValues(frame_->format());
if (plane_config == SkYUVAInfo::PlaneConfig::kUnknown)
return false;
// Don't use the YUV conversion path for multi-plane RGB frames.
if (frame_->format() == PIXEL_FORMAT_I444 &&
frame_->ColorSpace().GetMatrixID() == gfx::ColorSpace::MatrixID::GBR) {
return false;
}
if (!info)
return true;
SkYUVColorSpace yuv_color_space;
if (!frame_->ColorSpace().ToSkYUVColorSpace(frame_->BitDepth(),
&yuv_color_space)) {
// TODO(crbug.com/828599): This should default to BT.709 color space.
yuv_color_space = kRec601_SkYUVColorSpace;
}
// We use the Y plane size because it may get rounded up to an even size.
// Our implementation of GetYUVAPlanes expects this.
auto y_size = VideoFrame::PlaneSizeInSamples(
frame_->format(), VideoFrame::kYPlane, frame_->visible_rect().size());
auto yuva_info = SkYUVAInfo({y_size.width(), y_size.height()}, plane_config,
subsampling, yuv_color_space);
*info = SkYUVAPixmapInfo(yuva_info, SkYUVAPixmapInfo::DataType::kUnorm8,
/*rowBytes=*/nullptr);
return true;
}
bool GetYUVAPlanes(const SkYUVAPixmaps& pixmaps,
size_t frame_index,
uint32_t lazy_pixel_ref) override {
DCHECK_EQ(frame_index, 0u);
DCHECK_EQ(pixmaps.numPlanes(), 3);
#if DCHECK_IS_ON()
SkYUVAInfo::PlaneConfig plane_config;
SkYUVAInfo::Subsampling subsampling;
std::tie(plane_config, subsampling) =
VideoPixelFormatAsSkYUVAInfoValues(frame_->format());
DCHECK_EQ(plane_config, pixmaps.yuvaInfo().planeConfig());
DCHECK_EQ(subsampling, pixmaps.yuvaInfo().subsampling());
#endif
for (int plane = VideoFrame::kYPlane; plane <= VideoFrame::kVPlane;
++plane) {
const auto plane_size = VideoFrame::PlaneSizeInSamples(
frame_->format(), plane, frame_->visible_rect().size());
if (plane_size.width() != pixmaps.plane(plane).width() ||
plane_size.height() != pixmaps.plane(plane).height()) {
return false;
}
const auto& out_plane = pixmaps.plane(plane);
// Copy the frame to the supplied memory. It'd be nice to avoid this copy,
// but the memory is externally owned so we can't w/o an API change.
libyuv::CopyPlane(frame_->visible_data(plane), frame_->stride(plane),
reinterpret_cast<uint8_t*>(out_plane.writable_addr()),
out_plane.rowBytes(), plane_size.width(),
plane_size.height());
}
return true;
}
private:
scoped_refptr<VideoFrame> frame_;
DISALLOW_IMPLICIT_CONSTRUCTORS(VideoImageGenerator);
};
class VideoTextureBacking : public cc::TextureBacking {
public:
explicit VideoTextureBacking(
sk_sp<SkImage> sk_image,
const gpu::Mailbox& mailbox,
bool wraps_video_frame_texture,
scoped_refptr<viz::RasterContextProvider> raster_context_provider)
: sk_image_(std::move(sk_image)),
sk_image_info_(sk_image_->imageInfo()),
mailbox_(mailbox),
wraps_video_frame_texture_(wraps_video_frame_texture) {
raster_context_provider_ = std::move(raster_context_provider);
}
explicit VideoTextureBacking(
const gpu::Mailbox& mailbox,
const SkImageInfo& info,
bool wraps_video_frame_texture,
scoped_refptr<viz::RasterContextProvider> raster_context_provider)
: sk_image_info_(info),
mailbox_(mailbox),
wraps_video_frame_texture_(wraps_video_frame_texture) {
raster_context_provider_ = std::move(raster_context_provider);
}
~VideoTextureBacking() override {
auto* ri = raster_context_provider_->RasterInterface();
if (!wraps_video_frame_texture_) {
gpu::SyncToken sync_token;
ri->GenUnverifiedSyncTokenCHROMIUM(sync_token.GetData());
auto* sii = raster_context_provider_->SharedImageInterface();
sii->DestroySharedImage(sync_token, mailbox_);
}
}
const SkImageInfo& GetSkImageInfo() override { return sk_image_info_; }
gpu::Mailbox GetMailbox() const override { return mailbox_; }
sk_sp<SkImage> GetAcceleratedSkImage() override { return sk_image_; }
bool wraps_video_frame_texture() const { return wraps_video_frame_texture_; }
const scoped_refptr<viz::RasterContextProvider>& raster_context_provider()
const {
return raster_context_provider_;
}
// Used only for recycling this TextureBacking - where we need to keep the
// texture/mailbox alive, but replace the SkImage.
void ReplaceAcceleratedSkImage(sk_sp<SkImage> sk_image) {
sk_image_ = sk_image;
sk_image_info_ = sk_image->imageInfo();
}
sk_sp<SkImage> GetSkImageViaReadback() override {
if (sk_image_)
return sk_image_->makeNonTextureImage();
sk_sp<SkData> image_pixels =
SkData::MakeUninitialized(sk_image_info_.computeMinByteSize());
uint8_t* writable_pixels =
static_cast<uint8_t*>(image_pixels->writable_data());
gpu::raster::RasterInterface* ri =
raster_context_provider_->RasterInterface();
ri->ReadbackImagePixels(mailbox_, sk_image_info_,
sk_image_info_.minRowBytes(), 0, 0,
writable_pixels);
return SkImage::MakeRasterData(sk_image_info_, std::move(image_pixels),
sk_image_info_.minRowBytes());
}
bool readPixels(const SkImageInfo& dst_info,
void* dst_pixels,
size_t dst_row_bytes,
int src_x,
int src_y) override {
if (sk_image_) {
return sk_image_->readPixels(dst_info, dst_pixels, dst_row_bytes, src_x,
src_y);
}
gpu::raster::RasterInterface* ri =
raster_context_provider_->RasterInterface();
ri->ReadbackImagePixels(mailbox_, dst_info, dst_info.minRowBytes(), src_x,
src_y, dst_pixels);
return true;
}
void FlushPendingSkiaOps() override {
if (!raster_context_provider_ || !sk_image_)
return;
sk_image_->flushAndSubmit(raster_context_provider_->GrContext());
}
private:
sk_sp<SkImage> sk_image_;
SkImageInfo sk_image_info_;
scoped_refptr<viz::RasterContextProvider> raster_context_provider_;
// This can be either the source VideoFrame's texture (if
// |wraps_video_frame_texture_| is true) or a newly allocated shared image
// (if |wraps_video_frame_texture_| is false) if a copy or conversion was
// necessary.
const gpu::Mailbox mailbox_;
// Whether |mailbox_| directly points to a texture of the VideoFrame
// (if true), or to an allocated shared image (if false).
const bool wraps_video_frame_texture_;
};
PaintCanvasVideoRenderer::PaintCanvasVideoRenderer()
: cache_deleting_timer_(
FROM_HERE,
base::TimeDelta::FromSeconds(kTemporaryResourceDeletionDelay),
this,
&PaintCanvasVideoRenderer::ResetCache),
renderer_stable_id_(cc::PaintImage::GetNextId()) {}
PaintCanvasVideoRenderer::~PaintCanvasVideoRenderer() = default;
void PaintCanvasVideoRenderer::Paint(
scoped_refptr<VideoFrame> video_frame,
cc::PaintCanvas* canvas,
const gfx::RectF& dest_rect,
cc::PaintFlags& flags,
VideoTransformation video_transformation,
viz::RasterContextProvider* raster_context_provider) {
DCHECK(thread_checker_.CalledOnValidThread());
if (flags.getAlpha() == 0) {
return;
}
SkRect dest;
dest.setLTRB(dest_rect.x(), dest_rect.y(), dest_rect.right(),
dest_rect.bottom());
// Paint black rectangle if there isn't a frame available or the
// frame has an unexpected format.
if (!video_frame.get() || video_frame->natural_size().IsEmpty() ||
!(media::IsYuvPlanar(video_frame->format()) ||
video_frame->format() == PIXEL_FORMAT_Y16 ||
video_frame->format() == PIXEL_FORMAT_ARGB ||
video_frame->format() == PIXEL_FORMAT_XRGB ||
video_frame->format() == PIXEL_FORMAT_ABGR ||
video_frame->format() == PIXEL_FORMAT_XBGR ||
video_frame->HasTextures())) {
cc::PaintFlags black_with_alpha_flags;
black_with_alpha_flags.setAlpha(flags.getAlpha());
canvas->drawRect(dest, black_with_alpha_flags);
canvas->flush();
return;
}
// Don't allow wrapping the VideoFrame texture, as we want to be able to cache
// the PaintImage, to avoid redundant readbacks if the canvas is software.
if (!UpdateLastImage(video_frame, raster_context_provider,
false /* allow_wrap_texture */))
return;
DCHECK(cache_);
cc::PaintImage image = cache_->paint_image;
DCHECK(image);
absl::optional<ScopedSharedImageAccess> source_access;
if (video_frame->HasTextures() && cache_->source_texture) {
DCHECK(cache_->texture_backing);
source_access.emplace(raster_context_provider->RasterInterface(),
cache_->source_texture,
cache_->texture_backing->GetMailbox());
}
cc::PaintFlags video_flags;
video_flags.setAlpha(flags.getAlpha());
video_flags.setBlendMode(flags.getBlendMode());
const bool need_rotation = video_transformation.rotation != VIDEO_ROTATION_0;
const bool need_scaling =
dest_rect.size() != gfx::SizeF(video_frame->visible_rect().size());
const bool need_translation = !dest_rect.origin().IsOrigin();
const bool needs_y_flip = video_transformation.mirrored;
bool need_transform =
need_rotation || need_scaling || need_translation || needs_y_flip;
if (need_transform) {
canvas->save();
canvas->translate(
SkFloatToScalar(dest_rect.x() + (dest_rect.width() * 0.5f)),
SkFloatToScalar(dest_rect.y() + (dest_rect.height() * 0.5f)));
SkScalar angle = SkFloatToScalar(0.0f);
switch (video_transformation.rotation) {
case VIDEO_ROTATION_0:
break;
case VIDEO_ROTATION_90:
angle = SkFloatToScalar(90.0f);
break;
case VIDEO_ROTATION_180:
angle = SkFloatToScalar(180.0f);
break;
case VIDEO_ROTATION_270:
angle = SkFloatToScalar(270.0f);
break;
}
canvas->rotate(angle);
gfx::SizeF rotated_dest_size = dest_rect.size();
const bool has_flipped_size =
video_transformation.rotation == VIDEO_ROTATION_90 ||
video_transformation.rotation == VIDEO_ROTATION_270;
if (has_flipped_size) {
rotated_dest_size =
gfx::SizeF(rotated_dest_size.height(), rotated_dest_size.width());
}
auto sx = SkFloatToScalar(rotated_dest_size.width() /
video_frame->visible_rect().width());
auto sy = SkFloatToScalar(rotated_dest_size.height() /
video_frame->visible_rect().height());
if (needs_y_flip) {
if (has_flipped_size)
sy *= -1;
else
sx *= -1;
}
canvas->scale(sx, sy);
canvas->translate(
-SkFloatToScalar(video_frame->visible_rect().width() * 0.5f),
-SkFloatToScalar(video_frame->visible_rect().height() * 0.5f));
}
SkImageInfo info;
size_t row_bytes;
SkIPoint origin;
void* pixels = nullptr;
// This if is a special handling of video for SkiaPaintcanvas backend, where
// the video does not need any transform and it is enough to draw the frame
// directly into the skia canvas
if (!need_transform && video_frame->IsMappable() &&
flags.getAlpha() == SK_AlphaOPAQUE &&
flags.getBlendMode() == SkBlendMode::kSrc &&
flags.getFilterQuality() == kLow_SkFilterQuality &&
(pixels = canvas->accessTopLayerPixels(&info, &row_bytes, &origin)) &&
info.colorType() == kBGRA_8888_SkColorType) {
const size_t offset = info.computeOffset(origin.x(), origin.y(), row_bytes);
void* const pixels_offset = reinterpret_cast<char*>(pixels) + offset;
ConvertVideoFrameToRGBPixels(video_frame.get(), pixels_offset, row_bytes);
} else if (video_frame->HasTextures()) {
DCHECK_EQ(video_frame->coded_size(),
gfx::Size(image.width(), image.height()));
canvas->drawImageRect(
image, gfx::RectToSkRect(video_frame->visible_rect()),
SkRect::MakeWH(video_frame->visible_rect().width(),
video_frame->visible_rect().height()),
SkSamplingOptions(flags.getFilterQuality(),
SkSamplingOptions::kMedium_asMipmapLinear),
&video_flags, SkCanvas::kStrict_SrcRectConstraint);
} else {
DCHECK_EQ(video_frame->visible_rect().size(),
gfx::Size(image.width(), image.height()));
canvas->drawImage(
image, 0, 0,
SkSamplingOptions(flags.getFilterQuality(),
SkSamplingOptions::kMedium_asMipmapLinear),
&video_flags);
}
if (need_transform)
canvas->restore();
// Make sure to flush so we can remove the videoframe from the generator.
canvas->flush();
if (video_frame->HasTextures()) {
source_access.reset();
// Synchronize |video_frame| with the read operations in UpdateLastImage(),
// which are triggered by canvas->flush().
SynchronizeVideoFrameRead(std::move(video_frame),
raster_context_provider->RasterInterface(),
raster_context_provider->ContextSupport());
}
// Because we are not retaining a reference to the VideoFrame, it would be
// invalid for the texture_backing to directly wrap its texture(s), as they
// will be recycled.
DCHECK(!CacheBackingWrapsTexture());
}
void PaintCanvasVideoRenderer::Copy(
scoped_refptr<VideoFrame> video_frame,
cc::PaintCanvas* canvas,
viz::RasterContextProvider* raster_context_provider) {
cc::PaintFlags flags;
flags.setBlendMode(SkBlendMode::kSrc);
flags.setFilterQuality(kLow_SkFilterQuality);
auto dest_rect = gfx::RectF(gfx::SizeF(video_frame->visible_rect().size()));
Paint(std::move(video_frame), canvas, dest_rect, flags,
media::kNoTransformation, raster_context_provider);
}
namespace {
// libyuv doesn't support all 9-, 10- nor 12-bit pixel formats yet. This
// function creates a regular 8-bit video frame which we can give to libyuv.
scoped_refptr<VideoFrame> DownShiftHighbitVideoFrame(
const VideoFrame* video_frame) {
VideoPixelFormat format;
switch (video_frame->format()) {
case PIXEL_FORMAT_YUV420P12:
case PIXEL_FORMAT_YUV420P10:
case PIXEL_FORMAT_YUV420P9:
format = PIXEL_FORMAT_I420;
break;
case PIXEL_FORMAT_YUV422P12:
case PIXEL_FORMAT_YUV422P10:
case PIXEL_FORMAT_YUV422P9:
format = PIXEL_FORMAT_I422;
break;
case PIXEL_FORMAT_YUV444P12:
case PIXEL_FORMAT_YUV444P10:
case PIXEL_FORMAT_YUV444P9:
format = PIXEL_FORMAT_I444;
break;
default:
NOTREACHED();
return nullptr;
}
const int shift = video_frame->BitDepth() - 8;
scoped_refptr<VideoFrame> ret = VideoFrame::CreateFrame(
format, video_frame->coded_size(), video_frame->visible_rect(),
video_frame->natural_size(), video_frame->timestamp());
ret->set_color_space(video_frame->ColorSpace());
// Copy all metadata.
// (May be enough to copy color space)
ret->metadata().MergeMetadataFrom(video_frame->metadata());
for (int plane = VideoFrame::kYPlane; plane <= VideoFrame::kVPlane; ++plane) {
int width = ret->row_bytes(plane);
const uint16_t* src =
reinterpret_cast<const uint16_t*>(video_frame->data(plane));
uint8_t* dst = ret->data(plane);
if (!src) {
// An AV1 monochrome (grayscale) frame has no U and V planes. Set all U
// and V samples to the neutral value (128).
DCHECK_NE(plane, VideoFrame::kYPlane);
memset(dst, 128, ret->rows(plane) * ret->stride(plane));
continue;
}
for (int row = 0; row < video_frame->rows(plane); row++) {
for (int x = 0; x < width; x++) {
dst[x] = src[x] >> shift;
}
src += video_frame->stride(plane) / 2;
dst += ret->stride(plane);
}
}
return ret;
}
// Converts 16-bit data to |out| buffer of specified GL |type|.
// When the |format| is RGBA, the converted value is fed as luminance.
void FlipAndConvertY16(const VideoFrame* video_frame,
uint8_t* out,
unsigned format,
unsigned type,
bool flip_y,
size_t output_row_bytes) {
const uint8_t* row_head = video_frame->visible_data(0);
const size_t stride = video_frame->stride(0);
const int height = video_frame->visible_rect().height();
for (int i = 0; i < height; ++i, row_head += stride) {
uint8_t* out_row_head = flip_y ? out + output_row_bytes * (height - i - 1)
: out + output_row_bytes * i;
const uint16_t* row = reinterpret_cast<const uint16_t*>(row_head);
const uint16_t* row_end = row + video_frame->visible_rect().width();
if (type == GL_FLOAT) {
float* out_row = reinterpret_cast<float*>(out_row_head);
if (format == GL_RGBA) {
while (row < row_end) {
float gray_value = *row++ / 65535.f;
*out_row++ = gray_value;
*out_row++ = gray_value;
*out_row++ = gray_value;
*out_row++ = 1.0f;
}
continue;
} else if (format == GL_RED) {
while (row < row_end)
*out_row++ = *row++ / 65535.f;
continue;
}
// For other formats, hit NOTREACHED below.
} else if (type == GL_UNSIGNED_BYTE) {
// We take the upper 8 bits of 16-bit data and convert it as luminance to
// ARGB. We loose the precision here, but it is important not to render
// Y16 as RG_88. To get the full precision use float textures with WebGL1
// and e.g. R16UI or R32F textures with WebGL2.
DCHECK_EQ(static_cast<unsigned>(GL_RGBA), format);
uint32_t* rgba = reinterpret_cast<uint32_t*>(out_row_head);
while (row < row_end) {
uint32_t gray_value = *row++ >> 8;
*rgba++ = SkColorSetRGB(gray_value, gray_value, gray_value);
}
continue;
}
NOTREACHED() << "Unsupported Y16 conversion for format: 0x" << std::hex
<< format << " and type: 0x" << std::hex << type;
}
}
// Common functionality of PaintCanvasVideoRenderer's TexImage2D and
// TexSubImage2D. Allocates a buffer required for conversion and converts
// |frame| content to desired |format|. Returns true if calling glTex(Sub)Image
// is supported for provided |frame| format and parameters.
bool TexImageHelper(VideoFrame* frame,
unsigned format,
unsigned type,
bool flip_y,
scoped_refptr<DataBuffer>* temp_buffer) {
unsigned output_bytes_per_pixel = 0;
switch (frame->format()) {
case PIXEL_FORMAT_Y16:
// Converting single component unsigned short here to FLOAT luminance.
switch (format) {
case GL_RGBA:
if (type == GL_FLOAT) {
output_bytes_per_pixel = 4 * sizeof(GLfloat);
break;
}
return false;
case GL_RED:
if (type == GL_FLOAT) {
output_bytes_per_pixel = sizeof(GLfloat);
break;
}
return false;
default:
return false;
}
break;
default:
return false;
}
size_t output_row_bytes =
frame->visible_rect().width() * output_bytes_per_pixel;
*temp_buffer =
new DataBuffer(output_row_bytes * frame->visible_rect().height());
FlipAndConvertY16(frame, (*temp_buffer)->writable_data(), format, type,
flip_y, output_row_bytes);
return true;
}
// Upload the |frame| data to temporary texture of |temp_format|,
// |temp_internalformat| and |temp_type| and then copy intermediate texture
// subimage to destination |texture|. The destination |texture| is bound to the
// |target| before the call.
void TextureSubImageUsingIntermediate(unsigned target,
unsigned texture,
gpu::gles2::GLES2Interface* gl,
VideoFrame* frame,
int temp_internalformat,
unsigned temp_format,
unsigned temp_type,
int level,
int xoffset,
int yoffset,
bool flip_y,
bool premultiply_alpha) {
unsigned temp_texture = 0;
gl->GenTextures(1, &temp_texture);
gl->BindTexture(target, temp_texture);
gl->TexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
gl->TexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
gl->TexImage2D(target, 0, temp_internalformat, frame->visible_rect().width(),
frame->visible_rect().height(), 0, temp_format, temp_type,
frame->visible_data(0));
gl->BindTexture(target, texture);
gl->CopySubTextureCHROMIUM(temp_texture, 0, target, texture, level, 0, 0,
xoffset, yoffset, frame->visible_rect().width(),
frame->visible_rect().height(), flip_y,
premultiply_alpha, false);
gl->DeleteTextures(1, &temp_texture);
}
} // anonymous namespace
// static
void PaintCanvasVideoRenderer::ConvertVideoFrameToRGBPixels(
const VideoFrame* video_frame,
void* rgb_pixels,
size_t row_bytes,
bool premultiply_alpha) {
if (!video_frame->IsMappable()) {
NOTREACHED() << "Cannot extract pixels from non-CPU frame formats.";
return;
}
scoped_refptr<VideoFrame> temporary_frame;
// TODO(thomasanderson): Parallelize converting these formats.
switch (video_frame->format()) {
case PIXEL_FORMAT_YUV420P9:
case PIXEL_FORMAT_YUV422P9:
case PIXEL_FORMAT_YUV444P9:
case PIXEL_FORMAT_YUV444P10:
case PIXEL_FORMAT_YUV420P12:
case PIXEL_FORMAT_YUV422P12:
case PIXEL_FORMAT_YUV444P12:
temporary_frame = DownShiftHighbitVideoFrame(video_frame);
video_frame = temporary_frame.get();
break;
case PIXEL_FORMAT_YUV420P10:
// In AV1, a monochrome (grayscale) frame is represented as a YUV 4:2:0
// frame with no U and V planes. Since there are no 10-bit versions of
// libyuv::I400ToARGB() and libyuv::J400ToARGB(), convert the frame to an
// 8-bit YUV 4:2:0 frame with U and V planes.
if (!video_frame->data(VideoFrame::kUPlane) &&
!video_frame->data(VideoFrame::kVPlane)) {
temporary_frame = DownShiftHighbitVideoFrame(video_frame);
video_frame = temporary_frame.get();
}
break;
case PIXEL_FORMAT_Y16:
// Since it is grayscale conversion, we disregard
// SK_PMCOLOR_BYTE_ORDER and always use GL_RGBA.
FlipAndConvertY16(video_frame, static_cast<uint8_t*>(rgb_pixels), GL_RGBA,
GL_UNSIGNED_BYTE, false /*flip_y*/, row_bytes);
return;
default:
break;
}
constexpr size_t kTaskBytes = 1024 * 1024; // 1 MiB
const size_t n_tasks = std::min<size_t>(
std::max<size_t>(
1, VideoFrame::AllocationSize(video_frame->format(),
video_frame->visible_rect().size()) /
kTaskBytes),
base::SysInfo::NumberOfProcessors());
base::WaitableEvent event;
base::RepeatingClosure barrier = base::BarrierClosure(
n_tasks,
base::BindOnce(&base::WaitableEvent::Signal, base::Unretained(&event)));
for (size_t i = 1; i < n_tasks; ++i) {
base::ThreadPool::PostTask(
FROM_HERE,
base::BindOnce(ConvertVideoFrameToRGBPixelsTask,
base::Unretained(video_frame), rgb_pixels, row_bytes,
premultiply_alpha, i, n_tasks, &barrier));
}
ConvertVideoFrameToRGBPixelsTask(video_frame, rgb_pixels, row_bytes,
premultiply_alpha, 0, n_tasks, &barrier);
{
base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_wait;
event.Wait();
}
}
// static
void PaintCanvasVideoRenderer::CopyVideoFrameSingleTextureToGLTexture(
gpu::gles2::GLES2Interface* gl,
VideoFrame* video_frame,
unsigned int target,
unsigned int texture,
unsigned int internal_format,
unsigned int format,
unsigned int type,
int level,
bool premultiply_alpha,
bool flip_y) {
DCHECK(video_frame);
DCHECK(video_frame->HasTextures());
gpu::Mailbox mailbox;
uint32_t source_texture =
ImportVideoFrameSingleMailbox(gl, video_frame, &mailbox);
{
ScopedSharedImageAccess access(gl, source_texture, mailbox);
VideoFrameCopyTextureOrSubTexture(
gl, video_frame->coded_size(), video_frame->visible_rect(),
source_texture, target, texture, internal_format, format, type, level,
premultiply_alpha, flip_y);
}
gl->DeleteTextures(1, &source_texture);
gl->ShallowFlushCHROMIUM();
// The caller must call SynchronizeVideoFrameRead() after this operation, but
// we can't do that because we don't have the ContextSupport.
}
bool PaintCanvasVideoRenderer::CopyVideoFrameTexturesToGLTexture(
viz::RasterContextProvider* raster_context_provider,
gpu::gles2::GLES2Interface* destination_gl,
scoped_refptr<VideoFrame> video_frame,
unsigned int target,
unsigned int texture,
unsigned int internal_format,
unsigned int format,
unsigned int type,
int level,
bool premultiply_alpha,
bool flip_y) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(video_frame);
DCHECK(video_frame->HasTextures());
if (video_frame->NumTextures() > 1 ||
video_frame->metadata().read_lock_fences_enabled) {
if (!raster_context_provider)
return false;
GrDirectContext* gr_context = raster_context_provider->GrContext();
if (!gr_context &&
!raster_context_provider->ContextCapabilities().supports_oop_raster)
return false;
// Since skia always produces premultiply alpha outputs,
// trying direct uploading path when video format is opaque or premultiply
// alpha been requested. And dst texture mipLevel must be 0.
// TODO(crbug.com/1155003): Figure out whether premultiply options here are
// accurate.
if ((media::IsOpaque(video_frame->format()) || premultiply_alpha) &&
level == 0 && video_frame->NumTextures() > 1) {
if (UploadVideoFrameToGLTexture(raster_context_provider, destination_gl,
video_frame.get(), target, texture,
internal_format, format, type, flip_y)) {
return true;
}
}
if (!UpdateLastImage(video_frame, raster_context_provider,
true /* allow_wrap_texture */)) {
return false;
}
DCHECK(cache_);
DCHECK(cache_->texture_backing);
gpu::raster::RasterInterface* canvas_ri =
raster_context_provider->RasterInterface();
gpu::SyncToken sync_token;
// Wait for mailbox creation on canvas context before consuming it and
// copying from it on the consumer context.
canvas_ri->GenUnverifiedSyncTokenCHROMIUM(sync_token.GetData());
uint32_t intermediate_texture = SynchronizeAndImportMailbox(
destination_gl, sync_token, cache_->texture_backing->GetMailbox());
{
ScopedSharedImageAccess access(destination_gl, intermediate_texture,
cache_->texture_backing->GetMailbox());
VideoFrameCopyTextureOrSubTexture(
destination_gl, cache_->coded_size, cache_->visible_rect,
intermediate_texture, target, texture, internal_format, format, type,
level, premultiply_alpha, flip_y);
}
destination_gl->DeleteTextures(1, &intermediate_texture);
// Wait for destination context to consume mailbox before deleting it in
// canvas context.
gpu::SyncToken dest_sync_token;
destination_gl->GenUnverifiedSyncTokenCHROMIUM(dest_sync_token.GetData());
canvas_ri->WaitSyncTokenCHROMIUM(dest_sync_token.GetConstData());
// Because we are not retaining a reference to the VideoFrame, it would be
// invalid to keep the cache around if it directly wraps the VideoFrame
// texture(s), as they will be recycled.
if (cache_->texture_backing->wraps_video_frame_texture())
cache_.reset();
// Synchronize |video_frame| with the read operations in UpdateLastImage(),
// which are triggered by getBackendTexture() or CopyTextureCHROMIUM (in the
// case the cache was referencing its texture(s) directly).
SynchronizeVideoFrameRead(std::move(video_frame), canvas_ri,
raster_context_provider->ContextSupport());
} else {
CopyVideoFrameSingleTextureToGLTexture(
destination_gl, video_frame.get(), target, texture, internal_format,
format, type, level, premultiply_alpha, flip_y);
WaitAndReplaceSyncTokenClient client(destination_gl);
video_frame->UpdateReleaseSyncToken(&client);
}
DCHECK(!CacheBackingWrapsTexture());
return true;
}
bool PaintCanvasVideoRenderer::UploadVideoFrameToGLTexture(
viz::RasterContextProvider* raster_context_provider,
gpu::gles2::GLES2Interface* destination_gl,
scoped_refptr<VideoFrame> video_frame,
unsigned int target,
unsigned int texture,
unsigned int internal_format,
unsigned int format,
unsigned int type,
bool flip_y) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(video_frame);
// Support uploading for NV12 and I420 video frame only.
if (!VideoFrameYUVConverter::IsVideoFrameFormatSupported(*video_frame)) {
return false;
}
// TODO(crbug.com/1108154): Support more texture target, e.g.
// 2d array, 3d etc.
if (target != GL_TEXTURE_2D) {
return false;
}
if (!ValidFormatForDirectUploading(raster_context_provider,
static_cast<GLenum>(internal_format),
type)) {
return false;
}
// TODO(nazabris): Support OOP-R code path here that does not have GrContext.
if (!raster_context_provider || !raster_context_provider->GrContext())
return false;
// Trigger resource allocation for dst texture to back SkSurface.
// Dst texture size should equal to video frame visible rect.
destination_gl->BindTexture(target, texture);
destination_gl->TexImage2D(
target, 0, internal_format, video_frame->visible_rect().width(),
video_frame->visible_rect().height(), 0, format, type, nullptr);
gpu::MailboxHolder mailbox_holder;
mailbox_holder.texture_target = target;
destination_gl->ProduceTextureDirectCHROMIUM(texture,
mailbox_holder.mailbox.name);
destination_gl->GenUnverifiedSyncTokenCHROMIUM(
mailbox_holder.sync_token.GetData());
if (!VideoFrameYUVConverter::ConvertYUVVideoFrameToDstTextureNoCaching(
video_frame.get(), raster_context_provider, mailbox_holder,
internal_format, type, flip_y, true /* use visible_rect */)) {
return false;
}
gpu::raster::RasterInterface* source_ri =
raster_context_provider->RasterInterface();
// Wait for mailbox creation on canvas context before consuming it.
source_ri->GenUnverifiedSyncTokenCHROMIUM(
mailbox_holder.sync_token.GetData());
destination_gl->WaitSyncTokenCHROMIUM(
mailbox_holder.sync_token.GetConstData());
if (video_frame->HasTextures()) {
WaitAndReplaceSyncTokenClient client(source_ri);
video_frame->UpdateReleaseSyncToken(&client);
}
return true;
}
bool PaintCanvasVideoRenderer::PrepareVideoFrameForWebGL(
viz::RasterContextProvider* raster_context_provider,
gpu::gles2::GLES2Interface* destination_gl,
scoped_refptr<VideoFrame> video_frame,
unsigned int target,
unsigned int texture) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(video_frame);
DCHECK(video_frame->HasTextures());
if (video_frame->NumTextures() == 1) {
if (target == GL_TEXTURE_EXTERNAL_OES) {
// We don't support Android now.
// TODO(crbug.com/776222): support Android.
return false;
}
// We don't support sharing single video frame texture now.
// TODO(crbug.com/776222): deal with single video frame texture.
return false;
}
// TODO(nazabris): Support OOP-R code path here that does not have GrContext.
if (!raster_context_provider || !raster_context_provider->GrContext())
return false;
// Take webgl video texture as 2D texture. Setting it as external render
// target backend for skia.
destination_gl->BindTexture(target, texture);
destination_gl->TexImage2D(target, 0, GL_RGBA,
video_frame->coded_size().width(),
video_frame->coded_size().height(), 0, GL_RGBA,
GL_UNSIGNED_BYTE, nullptr);
gpu::raster::RasterInterface* source_ri =
raster_context_provider->RasterInterface();
gpu::MailboxHolder mailbox_holder;
mailbox_holder.texture_target = target;
destination_gl->ProduceTextureDirectCHROMIUM(texture,
mailbox_holder.mailbox.name);
destination_gl->GenUnverifiedSyncTokenCHROMIUM(
mailbox_holder.sync_token.GetData());
if (!PrepareVideoFrame(video_frame, raster_context_provider,
mailbox_holder)) {
return false;
}
// Wait for mailbox creation on canvas context before consuming it and
// copying from it on the consumer context.
source_ri->GenUnverifiedSyncTokenCHROMIUM(
mailbox_holder.sync_token.GetData());
destination_gl->WaitSyncTokenCHROMIUM(
mailbox_holder.sync_token.GetConstData());
WaitAndReplaceSyncTokenClient client(source_ri);
video_frame->UpdateReleaseSyncToken(&client);
DCHECK(!CacheBackingWrapsTexture());
return true;
}
bool PaintCanvasVideoRenderer::CopyVideoFrameYUVDataToGLTexture(
viz::RasterContextProvider* raster_context_provider,
gpu::gles2::GLES2Interface* destination_gl,
scoped_refptr<VideoFrame> video_frame,
unsigned int target,
unsigned int texture,
unsigned int internal_format,
unsigned int format,
unsigned int type,
int level,
bool premultiply_alpha,
bool flip_y) {
if (!raster_context_provider)
return false;
#if defined(OS_ANDROID)
// TODO(crbug.com/1181993): These formats don't work with the passthrough
// command decoder on Android for some reason.
const auto format_enum = static_cast<GLenum>(internal_format);
if (format_enum == GL_RGB10_A2 || format_enum == GL_RGB565 ||
format_enum == GL_RGB5_A1 || format_enum == GL_RGBA4) {
return false;
}
#endif
if (!video_frame->IsMappable()) {
return false;
}
if (!VideoFrameYUVConverter::IsVideoFrameFormatSupported(*video_frame)) {
return false;
}
// Could handle NV12 here as well. See NewSkImageFromVideoFrameYUV.
// Since skia always produces premultiply alpha outputs,
// trying direct uploading path when video format is opaque or premultiply
// alpha been requested. And dst texture mipLevel must be 0.
// TODO(crbug.com/1155003): Figure out whether premultiply options here are
// accurate.
if ((media::IsOpaque(video_frame->format()) || premultiply_alpha) &&
level == 0) {
if (UploadVideoFrameToGLTexture(raster_context_provider, destination_gl,
video_frame, target, texture,
internal_format, format, type, flip_y)) {
return true;
}
}
auto* sii = raster_context_provider->SharedImageInterface();
gpu::raster::RasterInterface* source_ri =
raster_context_provider->RasterInterface();
// We need a shared image to receive the intermediate RGB result. Try to reuse
// one if compatible, otherwise create a new one.
gpu::SyncToken token;
if (!yuv_cache_.mailbox.IsZero() &&
yuv_cache_.size == video_frame->coded_size() &&
yuv_cache_.raster_context_provider == raster_context_provider) {
token = yuv_cache_.sync_token;
} else {
yuv_cache_.Reset();
yuv_cache_.raster_context_provider = raster_context_provider;
yuv_cache_.size = video_frame->coded_size();
uint32_t usage = gpu::SHARED_IMAGE_USAGE_GLES2;
if (raster_context_provider->ContextCapabilities().supports_oop_raster) {
usage |= gpu::SHARED_IMAGE_USAGE_RASTER |
gpu::SHARED_IMAGE_USAGE_OOP_RASTERIZATION;
}
yuv_cache_.mailbox = sii->CreateSharedImage(
viz::ResourceFormat::RGBA_8888, video_frame->coded_size(),
gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType, usage,
gpu::kNullSurfaceHandle);
token = sii->GenUnverifiedSyncToken();
}
// On the source Raster context, do the YUV->RGB conversion.
gpu::MailboxHolder dest_holder;
dest_holder.mailbox = yuv_cache_.mailbox;
dest_holder.texture_target = GL_TEXTURE_2D;
dest_holder.sync_token = token;
yuv_cache_.yuv_converter.ConvertYUVVideoFrame(
video_frame.get(), raster_context_provider, dest_holder);
gpu::SyncToken post_conversion_sync_token;
source_ri->GenUnverifiedSyncTokenCHROMIUM(
post_conversion_sync_token.GetData());
// On the destination GL context, do a copy (with cropping) into the
// destination texture.
GLuint intermediate_texture = SynchronizeAndImportMailbox(
destination_gl, post_conversion_sync_token, yuv_cache_.mailbox);
{
ScopedSharedImageAccess access(destination_gl, intermediate_texture,
yuv_cache_.mailbox);
VideoFrameCopyTextureOrSubTexture(
destination_gl, video_frame->coded_size(), video_frame->visible_rect(),
intermediate_texture, target, texture, internal_format, format, type,
level, premultiply_alpha, flip_y);
}
destination_gl->DeleteTextures(1, &intermediate_texture);
destination_gl->GenUnverifiedSyncTokenCHROMIUM(
yuv_cache_.sync_token.GetData());
// video_frame->UpdateReleaseSyncToken is not necessary since the video frame
// data we used was CPU-side (IsMappable) to begin with. If there were any
// textures, we didn't use them.
// The temporary SkImages should be automatically cleaned up here.
// Kick off a timer to release the cache.
cache_deleting_timer_.Reset();
return true;
}
bool PaintCanvasVideoRenderer::TexImage2D(
unsigned target,
unsigned texture,
gpu::gles2::GLES2Interface* gl,
const gpu::Capabilities& gpu_capabilities,
VideoFrame* frame,
int level,
int internalformat,
unsigned format,
unsigned type,
bool flip_y,
bool premultiply_alpha) {
DCHECK(frame);
DCHECK(!frame->HasTextures());
// Note: CopyTextureCHROMIUM uses mediump for color computation. Don't use
// it if the precision would lead to data loss when converting 16-bit
// normalized to float. medium_float.precision > 15 means that the approach
// below is not used on Android, where the extension EXT_texture_norm16 is
// not widely supported. It is used on Windows, Linux and OSX.
// Android support is not required for now because Tango depth camera already
// provides floating point data (projected point cloud). See crbug.com/674440.
if (gpu_capabilities.texture_norm16 &&
gpu_capabilities.fragment_shader_precisions.medium_float.precision > 15 &&
target == GL_TEXTURE_2D &&
(type == GL_FLOAT || type == GL_UNSIGNED_BYTE)) {
// TODO(aleksandar.stojiljkovic): Extend the approach to TexSubImage2D
// implementation and other types. See https://crbug.com/624436.
// Allocate the destination texture.
gl->TexImage2D(target, level, internalformat, frame->visible_rect().width(),
frame->visible_rect().height(), 0, format, type, nullptr);
// We use sized internal format GL_R16_EXT instead of unsized GL_RED.
// See angleproject:1952
TextureSubImageUsingIntermediate(target, texture, gl, frame, GL_R16_EXT,
GL_RED, GL_UNSIGNED_SHORT, level, 0, 0,
flip_y, premultiply_alpha);
return true;
}
scoped_refptr<DataBuffer> temp_buffer;
if (!TexImageHelper(frame, format, type, flip_y, &temp_buffer))
return false;
gl->TexImage2D(target, level, internalformat, frame->visible_rect().width(),
frame->visible_rect().height(), 0, format, type,
temp_buffer->data());
return true;
}
bool PaintCanvasVideoRenderer::TexSubImage2D(unsigned target,
gpu::gles2::GLES2Interface* gl,
VideoFrame* frame,
int level,
unsigned format,
unsigned type,
int xoffset,
int yoffset,
bool flip_y,
bool premultiply_alpha) {
DCHECK(frame);
DCHECK(!frame->HasTextures());
scoped_refptr<DataBuffer> temp_buffer;
if (!TexImageHelper(frame, format, type, flip_y, &temp_buffer))
return false;
gl->TexSubImage2D(
target, level, xoffset, yoffset, frame->visible_rect().width(),
frame->visible_rect().height(), format, type, temp_buffer->data());
return true;
}
void PaintCanvasVideoRenderer::ResetCache() {
DCHECK(thread_checker_.CalledOnValidThread());
cache_.reset();
yuv_cache_.Reset();
}
PaintCanvasVideoRenderer::Cache::Cache(int frame_id) : frame_id(frame_id) {}
PaintCanvasVideoRenderer::Cache::~Cache() = default;
bool PaintCanvasVideoRenderer::Cache::Recycle() {
DCHECK(!texture_backing->wraps_video_frame_texture());
paint_image = cc::PaintImage();
if (!texture_backing->unique())
return false;
// Flush any pending GPU work using this texture.
texture_backing->FlushPendingSkiaOps();
return true;
}
bool PaintCanvasVideoRenderer::UpdateLastImage(
scoped_refptr<VideoFrame> video_frame,
viz::RasterContextProvider* raster_context_provider,
bool allow_wrap_texture) {
DCHECK(!CacheBackingWrapsTexture());
if (!cache_ || video_frame->unique_id() != cache_->frame_id ||
!cache_->paint_image) {
auto paint_image_builder =
cc::PaintImageBuilder::WithDefault()
.set_id(renderer_stable_id_)
.set_animation_type(cc::PaintImage::AnimationType::VIDEO)
.set_completion_state(cc::PaintImage::CompletionState::DONE);
// Generate a new image.
// Note: Skia will hold onto |video_frame| via |video_generator| only when
// |video_frame| is software.
// Holding |video_frame| longer than this call when using GPUVideoDecoder
// could cause problems since the pool of VideoFrames has a fixed size.
if (video_frame->HasTextures()) {
DCHECK(raster_context_provider);
bool supports_oop_raster =
raster_context_provider->ContextCapabilities().supports_oop_raster;
DCHECK(supports_oop_raster || raster_context_provider->GrContext());
auto* ri = raster_context_provider->RasterInterface();
DCHECK(ri);
bool wraps_video_frame_texture = false;
gpu::Mailbox mailbox;
if (allow_wrap_texture && video_frame->NumTextures() == 1) {
cache_.emplace(video_frame->unique_id());
const gpu::MailboxHolder& holder =
GetVideoFrameMailboxHolder(video_frame.get());
mailbox = holder.mailbox;
ri->WaitSyncTokenCHROMIUM(holder.sync_token.GetConstData());
wraps_video_frame_texture = true;
} else {
if (cache_ && cache_->texture_backing &&
cache_->texture_backing->raster_context_provider() ==
raster_context_provider &&
cache_->coded_size == video_frame->coded_size() &&
cache_->Recycle()) {
// We can reuse the shared image from the previous cache.
cache_->frame_id = video_frame->unique_id();
mailbox = cache_->texture_backing->GetMailbox();
} else {
cache_.emplace(video_frame->unique_id());
auto* sii = raster_context_provider->SharedImageInterface();
// TODO(nazabris): Sort out what to do when GLES2 is needed but the
// cached shared image is created without it.
uint32_t flags =
gpu::SHARED_IMAGE_USAGE_GLES2 | gpu::SHARED_IMAGE_USAGE_RASTER;
if (supports_oop_raster) {
flags |= gpu::SHARED_IMAGE_USAGE_OOP_RASTERIZATION;
}
mailbox = sii->CreateSharedImage(
viz::ResourceFormat::RGBA_8888, video_frame->coded_size(),
gfx::ColorSpace(), kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType,
flags, gpu::kNullSurfaceHandle);
ri->WaitSyncTokenCHROMIUM(
sii->GenUnverifiedSyncToken().GetConstData());
}
if (video_frame->NumTextures() == 1) {
auto frame_mailbox =
SynchronizeVideoFrameSingleMailbox(ri, video_frame.get());
ri->CopySubTexture(frame_mailbox, mailbox, GL_TEXTURE_2D, 0, 0, 0, 0,
video_frame->coded_size().width(),
video_frame->coded_size().height(), GL_FALSE,
GL_FALSE);
} else {
gpu::MailboxHolder dest_holder{mailbox, gpu::SyncToken(),
GL_TEXTURE_2D};
VideoFrameYUVConverter::ConvertYUVVideoFrameNoCaching(
video_frame.get(), raster_context_provider, dest_holder);
}
if (!supports_oop_raster)
raster_context_provider->GrContext()->flushAndSubmit();
}
cache_->coded_size = video_frame->coded_size();
cache_->visible_rect = video_frame->visible_rect();
// In OOPR mode, we can keep the entire TextureBacking. In non-OOPR,
// we can recycle the mailbox/texture, but have to replace the SkImage.
if (!supports_oop_raster) {
cache_->source_texture = ri->CreateAndConsumeForGpuRaster(mailbox);
// TODO(nazabris): Handle scoped access correctly. This follows the
// current pattern but is most likely bugged. Access should last for
// the lifetime of the SkImage.
ScopedSharedImageAccess(ri, cache_->source_texture, mailbox);
auto source_image =
WrapGLTexture(wraps_video_frame_texture
? video_frame->mailbox_holder(0).texture_target
: GL_TEXTURE_2D,
cache_->source_texture, video_frame->coded_size(),
raster_context_provider);
if (!source_image) {
// Couldn't create the SkImage.
cache_.reset();
return false;
}
if (!cache_->texture_backing) {
cache_->texture_backing = sk_make_sp<VideoTextureBacking>(
std::move(source_image), mailbox, wraps_video_frame_texture,
raster_context_provider);
} else {
cache_->texture_backing->ReplaceAcceleratedSkImage(
std::move(source_image));
}
} else if (!cache_->texture_backing) {
SkImageInfo sk_image_info =
SkImageInfo::Make(gfx::SizeToSkISize(cache_->coded_size),
kRGBA_8888_SkColorType, kPremul_SkAlphaType);
cache_->texture_backing = sk_make_sp<VideoTextureBacking>(
mailbox, sk_image_info, wraps_video_frame_texture,
raster_context_provider);
}
paint_image_builder.set_texture_backing(
cache_->texture_backing, cc::PaintImage::GetNextContentId());
} else {
cache_.emplace(video_frame->unique_id());
paint_image_builder.set_paint_image_generator(
sk_make_sp<VideoImageGenerator>(video_frame));
}
cache_->paint_image = paint_image_builder.TakePaintImage();
if (!cache_->paint_image) {
// Couldn't create the PaintImage.
cache_.reset();
return false;
}
}
DCHECK(cache_);
cache_deleting_timer_.Reset();
return true;
}
bool PaintCanvasVideoRenderer::PrepareVideoFrame(
scoped_refptr<VideoFrame> video_frame,
viz::RasterContextProvider* raster_context_provider,
const gpu::MailboxHolder& dest_holder) {
// Generate a new image.
// Note: Skia will hold onto |video_frame| via |video_generator| only when
// |video_frame| is software.
// Holding |video_frame| longer than this call when using GPUVideoDecoder
// could cause problems since the pool of VideoFrames has a fixed size.
if (video_frame->HasTextures()) {
if (video_frame->NumTextures() > 1) {
VideoFrameYUVConverter::ConvertYUVVideoFrameNoCaching(
video_frame.get(), raster_context_provider, dest_holder);
} else {
// We don't support Android now.
return false;
}
}
return true;
}
PaintCanvasVideoRenderer::YUVTextureCache::YUVTextureCache() = default;
PaintCanvasVideoRenderer::YUVTextureCache::~YUVTextureCache() {
Reset();
}
void PaintCanvasVideoRenderer::YUVTextureCache::Reset() {
if (mailbox.IsZero())
return;
DCHECK(raster_context_provider);
gpu::raster::RasterInterface* ri = raster_context_provider->RasterInterface();
ri->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
ri->OrderingBarrierCHROMIUM();
auto* sii = raster_context_provider->SharedImageInterface();
sii->DestroySharedImage(sync_token, mailbox);
mailbox.SetZero();
yuv_converter.ReleaseCachedData();
// Kick off the GL work up to the OrderingBarrierCHROMIUM above as well as the
// SharedImageInterface work, to ensure the shared image memory is released in
// a timely fashion.
raster_context_provider->ContextSupport()->FlushPendingWork();
raster_context_provider.reset();
}
gfx::Size PaintCanvasVideoRenderer::LastImageDimensionsForTesting() {
DCHECK(cache_);
DCHECK(cache_->paint_image);
return gfx::Size(cache_->paint_image.width(), cache_->paint_image.height());
}
bool PaintCanvasVideoRenderer::CacheBackingWrapsTexture() const {
return cache_ && cache_->texture_backing &&
cache_->texture_backing->wraps_video_frame_texture();
}
} // namespace media