0

Zero-copy: introduce a way to letterbox the BlitRequest results

Currently, CopyOutputRequest API enables its callers to obtain a copy
of the contents produced by a render pass. BlitRequest allows those
results to be placed in a specific region of caller-provided textures,
without modifying contents outside of that region. This is an
almost-ideal situation for FrameSinkVideoCapturerImpl, with an
exception that when it produces a VideoFrame, it may be forced to
letterbox part of the frame. To avoid doing this work in the capturer
(by memory-mapping the GpuMemoryBuffer), a letterbox region is
introduced to BlitRequest.

Changes:
- Introduce `BlitRequest::letterbox_region()` that specifies the
  region of the caller-provided textures outside of which everything
  will be filled with black.
- `FrameSinkVideoCaprturerImpl` takes advantage of newly introduced
  capability on `BlitRequest`. Letterboxing only happens in the
  capturer for non-GMB-backed VideoFrames, and only if there are parts
  of a frame that we have not already written to.
- `skia::BlitRGBAToYUVA()` now exposes additional parameter to allow
  letterboxing of the destination image.
- Test changes in SkiaReadbackPixeltest to account for new parameter
  to `BlitRequest`.
- Minor: `RenderableGpuMemoryBufferVideoFramePool` now tags
  GpuMemoryBuffers with the color space.
- Minor cleanup in
  `SkiaOutputSurfaceImplOnGpu::ImportSurfacesForNV12Planes()` - rename
  variables, add/expand comments, add DVLOG(3)s to facilitate
  future investigations.
- Minor cleanup in `FrameSinkVideoCapturerImpl` - comments, DVLOGs,
  helpers.

Bug: 1310411
Change-Id: I292ab78b9aabc29e23eb1489ea1ca8c9752b88eb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3617204
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Reviewed-by: Dan Sanders <sandersd@chromium.org>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Piotr Bialecki <bialpio@chromium.org>
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1003391}
This commit is contained in:
Piotr Bialecki
2022-05-13 23:18:03 +00:00
committed by Chromium LUCI CQ
parent 15e98b540a
commit a88518eecf
13 changed files with 311 additions and 96 deletions

@ -56,8 +56,8 @@ properties:
Note that all coordinates are constrained to be integer values, to avoid
introducing alignment, rounding or other "fuzz" issues.
* Result format: An RGBA-interleaved bitmap (SkBitmap) or I420 Y+U+V image
planes.
* Result format: An RGBA-interleaved bitmap (SkBitmap), I420 Y+U+V image
planes, or NV12 Y+UV image planes.
For efficient video capture, the above are used as follows: An issuer of
CopyOutputRequests "locks into" a target area within the Surface (usually the

@ -31,9 +31,11 @@ std::string BlendBitmap::ToString() const {
BlitRequest::BlitRequest(
const gfx::Point& destination_region_offset,
LetterboxingBehavior letterboxing_behavior,
const std::array<gpu::MailboxHolder, CopyOutputResult::kMaxPlanes>&
mailboxes)
: destination_region_offset_(destination_region_offset),
letterboxing_behavior_(letterboxing_behavior),
mailboxes_(mailboxes) {}
BlitRequest::BlitRequest(BlitRequest&& other) = default;

@ -53,6 +53,16 @@ class VIZ_COMMON_EXPORT BlendBitmap {
sk_sp<SkImage> image_;
};
// Enum used to specify letteboxing behavior for a BlitRequest.
enum class LetterboxingBehavior {
// No letterboxing is needed - only the destination region will be written
// into by the handler of CopyOutputRequest.
kDoNotLetterbox,
// Letterboxing is needed - everything outside of the destination region
// will be filled with black by the handler of CopyOutputRequest.
kLetterbox
};
// Structure describing a blit operation that can be appended to
// `CopyOutputRequest` if the callers want to place the results of the operation
// in textures that they own.
@ -60,6 +70,7 @@ class VIZ_COMMON_EXPORT BlitRequest {
public:
explicit BlitRequest(
const gfx::Point& destination_region_offset,
LetterboxingBehavior letterboxing_behavior,
const std::array<gpu::MailboxHolder, CopyOutputResult::kMaxPlanes>&
mailboxes);
@ -74,6 +85,10 @@ class VIZ_COMMON_EXPORT BlitRequest {
return destination_region_offset_;
}
LetterboxingBehavior letterboxing_behavior() const {
return letterboxing_behavior_;
}
const std::array<gpu::MailboxHolder, CopyOutputResult::kMaxPlanes>&
mailboxes() const {
return mailboxes_;
@ -106,6 +121,9 @@ class VIZ_COMMON_EXPORT BlitRequest {
// images.
gfx::Point destination_region_offset_;
// Specifies the letterboxing behavior of this request.
LetterboxingBehavior letterboxing_behavior_;
// Mailboxes with planes that will be populated.
// The textures can (but don't have to be) backed by
// a GpuMemoryBuffer. The pixel format of the request determines

@ -107,6 +107,7 @@ void CopyOutputRequest::set_blit_request(BlitRequest blit_request) {
DCHECK(!blit_request_);
DCHECK_EQ(result_destination(), ResultDestination::kNativeTextures);
DCHECK_EQ(result_format(), ResultFormat::NV12_PLANES);
DCHECK(has_result_selection());
// Destination region must start at an even offset for NV12 results:
DCHECK_EQ(blit_request.destination_region_offset().x() % 2, 0);

@ -116,12 +116,14 @@ class VIZ_COMMON_EXPORT CopyOutputRequest {
// Optionally specify that only a portion of the result be generated. The
// selection rect will be clamped to the result bounds, which always starts at
// 0,0 and spans the post-scaling size of the copy area (see set_area()
// above). Only RGBA format supports odd-sized result selection.
// above). Only RGBA format supports odd-sized result selection. Can only be
// called before blit request was set on the copy request.
void set_result_selection(const gfx::Rect& selection) {
DCHECK(result_format_ == ResultFormat::RGBA ||
(selection.width() % 2 == 0 && selection.height() % 2 == 0))
<< "CopyOutputRequest supports odd-sized result_selection() only for "
"RGBA!";
DCHECK(!has_blit_request());
result_selection_ = selection;
}
bool has_result_selection() const { return result_selection_.has_value(); }
@ -129,7 +131,15 @@ class VIZ_COMMON_EXPORT CopyOutputRequest {
// Requests that the region copied by the CopyOutputRequest be blitted into
// the caller's textures. Can be called only for CopyOutputRequests that
// target native textures.
// target native textures. Requires that result selection was set, in which
// case the caller's textures will be populated with the results of the
// copy request. The region in the caller's textures that will be populated
// is specified by `gfx::Rect(blit_request.destination_region_offset(),
// result_selection().size())`. If blit request is configured to perform
// letterboxing, all contents outside of that region will be overwritten with
// black, otherwise they will be unchanged. If the copy request's result would
// be smaller than `result_selection().size()`, the request will fail (i.e.
// empty result will be sent).
void set_blit_request(BlitRequest blit_request);
bool has_blit_request() const { return blit_request_.has_value(); }
const BlitRequest& blit_request() const { return *blit_request_; }

@ -17,6 +17,7 @@
#include "cc/test/pixel_test.h"
#include "cc/test/pixel_test_utils.h"
#include "cc/test/resource_provider_test_utils.h"
#include "components/viz/common/frame_sinks/blit_request.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "components/viz/common/frame_sinks/copy_output_util.h"
@ -572,7 +573,8 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(SkiaReadbackPixelTestNV12);
class SkiaReadbackPixelTestNV12WithBlit
: public SkiaReadbackPixelTest,
public testing::WithParamInterface<bool> {
public testing::WithParamInterface<
std::tuple<bool, LetterboxingBehavior>> {
public:
CopyOutputResult::Destination RequestDestination() const {
return CopyOutputResult::Destination::kNativeTextures;
@ -583,7 +585,11 @@ class SkiaReadbackPixelTestNV12WithBlit
}
void SetUp() override {
SkiaReadbackPixelTest::SetUpReadbackPixeltest(GetParam());
SkiaReadbackPixelTest::SetUpReadbackPixeltest(std::get<0>(GetParam()));
}
LetterboxingBehavior GetLetterboxingBehavior() const {
return std::get<1>(GetParam());
}
};
@ -625,8 +631,7 @@ TEST_P(SkiaReadbackPixelTestNV12WithBlit, ExecutesCopyRequestWithBlit) {
<< " The test case expects the blit region's origin to be even for NV12 "
"blit requests";
const SkColor rgba_red = SkColorSetARGB(0xff, 0xff, 0, 0);
const SkColor yuv_red = GLScalerTestUtil::ConvertRGBAColorToYUV(rgba_red);
const SkColor yuv_red = GLScalerTestUtil::ConvertRGBAColorToYUV(SK_ColorRED);
const std::vector<uint8_t> luma_pattern = {
static_cast<uint8_t>(SkColorGetR(yuv_red))};
@ -669,8 +674,9 @@ TEST_P(SkiaReadbackPixelTestNV12WithBlit, ExecutesCopyRequestWithBlit) {
request.set_result_selection(result_selection);
request.set_blit_request(
BlitRequest(destination_subregion.origin(), mailboxes));
request.set_blit_request(BlitRequest(destination_subregion.origin(),
GetLetterboxingBehavior(),
mailboxes));
}));
// Check that a result was produced and is of the expected rect/size.
@ -716,7 +722,18 @@ TEST_P(SkiaReadbackPixelTestNV12WithBlit, ExecutesCopyRequestWithBlit) {
// The textures that we passed in to BlitRequest contained NV12 plane data for
// an all-red image, let's re-create such a bitmap:
SkBitmap expected = GLScalerTestUtil::AllocateRGBABitmap(source_size);
expected.eraseColor(rgba_red);
if (GetLetterboxingBehavior() == LetterboxingBehavior::kLetterbox) {
// We have requested the results to be letterboxed, so everything that
// CopyOutputRequest is not populating w/ render pass contents should be
// black:
expected.eraseColor(SK_ColorBLACK);
} else {
// We have requested the results to not be letterboxed, so everything that
// CopyOutputRequest is not populating w/ render pass will have original
// contents (red in our case):
expected.eraseColor(SK_ColorRED);
}
// Blit request should "stitch" the pixels from the source image into a
// sub-region of caller-provided texture - let's write our expected pixels
@ -732,10 +749,12 @@ TEST_P(SkiaReadbackPixelTestNV12WithBlit, ExecutesCopyRequestWithBlit) {
}
#if !BUILDFLAG(IS_ANDROID) || !defined(ARCH_CPU_X86_FAMILY)
INSTANTIATE_TEST_SUITE_P(,
SkiaReadbackPixelTestNV12WithBlit,
// Result scaling: Scale by half?
testing::Values(true, false));
INSTANTIATE_TEST_SUITE_P(
,
SkiaReadbackPixelTestNV12WithBlit,
testing::Combine(testing::Bool(), // Result scaling: Scale by half?
testing::Values(LetterboxingBehavior::kDoNotLetterbox,
LetterboxingBehavior::kLetterbox)));
#else
// Don't instantiate the NV12 tests when run on Android emulator, they won't
// work since the SkiaRenderer currently does not support CopyOutputRequests

@ -849,8 +849,8 @@ bool SkiaOutputSurfaceImplOnGpu::ImportSurfacesForNV12Planes(
for (size_t i = 0; i < CopyOutputResult::kNV12MaxPlanes; ++i) {
const gpu::MailboxHolder& mailbox_holder = blit_request.mailbox(i);
// Should never happen, maiboxes are validated when setting blit request on
// a CopyOutputResult.
// Should never happen, mailboxes are validated when setting blit request on
// a CopyOutputResult and we only access `kNV12MaxPlanes` mailboxes.
DCHECK(!mailbox_holder.mailbox.IsZero());
PlaneAccessData& plane_data = plane_access_datas[i];
@ -912,6 +912,9 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
CopyOutputRequest::ResultDestination::kNativeTextures)
<< "Only CopyOutputRequests that hand out native textures support blit "
"requests!";
DCHECK(!request->has_blit_request() || request->has_result_selection())
<< "Only CopyOutputRequests that specify result selection support blit "
"requests!";
// Overview:
// 1. Try to create surfaces for NV12 planes (we know the needed size in
@ -934,16 +937,29 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
// copied, as well as area, scaling & result_selection of the `request`.
// This represents the size of the intermediate texture that will be then
// blitted to the destination textures.
const gfx::Size destination_size = geometry.result_selection.size();
const gfx::Size intermediate_dst_size = geometry.result_selection.size();
std::array<PlaneAccessData, CopyOutputResult::kNV12MaxPlanes>
plane_access_datas;
SkYUVAInfo yuva_info;
bool destination_surfaces_created = false;
bool destination_surfaces_ready = false;
if (request->has_blit_request()) {
destination_surfaces_created = ImportSurfacesForNV12Planes(
if (request->result_selection().size() != intermediate_dst_size) {
DLOG(WARNING)
<< __func__
<< ": result selection is different than render pass output, "
"geometry="
<< geometry.ToString() << ", request=" << request->ToString();
// Send empty result, we have a blit request that asks for a different
// size than what we have available - the behavior in this case is
// currently unspecified as we'd have to leave parts of the caller's
// region unpopulated.
return;
}
destination_surfaces_ready = ImportSurfacesForNV12Planes(
request->blit_request(), plane_access_datas);
// The entire destination image size is the same as the size of the luma
@ -954,7 +970,8 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
// Check if the destination will fit in the blit target:
const gfx::Rect blit_destination_rect(
request->blit_request().destination_region_offset(), destination_size);
request->blit_request().destination_region_offset(),
intermediate_dst_size);
const gfx::Rect blit_target_image_rect(
gfx::SkISizeToSize(plane_access_datas[0].size));
@ -964,24 +981,25 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
return;
}
} else {
yuva_info = SkYUVAInfo(
gfx::SizeToSkISize(destination_size), SkYUVAInfo::PlaneConfig::kY_UV,
SkYUVAInfo::Subsampling::k420, kRec709_Limited_SkYUVColorSpace);
yuva_info = SkYUVAInfo(gfx::SizeToSkISize(intermediate_dst_size),
SkYUVAInfo::PlaneConfig::kY_UV,
SkYUVAInfo::Subsampling::k420,
kRec709_Limited_SkYUVColorSpace);
destination_surfaces_created =
destination_surfaces_ready =
CreateSurfacesForNV12Planes(yuva_info, color_space, plane_access_datas);
}
if (!destination_surfaces_created) {
DVLOG(1) << "failed to create destination surfaces";
if (!destination_surfaces_ready) {
DVLOG(1) << "failed to create / import destination surfaces";
// Send empty result.
return;
}
// Create a destination for the scaled & clipped result:
auto representation = CreateSharedImageRepresentationSkia(
ResourceFormat::RGBA_8888, destination_size, color_space);
if (!representation) {
auto intermediate_representation = CreateSharedImageRepresentationSkia(
ResourceFormat::RGBA_8888, intermediate_dst_size, color_space);
if (!intermediate_representation) {
DVLOG(1) << "failed to create shared image representation for the "
"intermediate surface";
// Send empty result.
@ -992,9 +1010,11 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
std::vector<GrBackendSemaphore> begin_semaphores;
std::vector<GrBackendSemaphore> end_semaphores;
auto scoped_write = representation->BeginScopedWriteAccess(
/*final_msaa_count=*/1, surface_props, &begin_semaphores, &end_semaphores,
gpu::SharedImageRepresentation::AllowUnclearedAccess::kYes);
auto intermediate_scoped_write =
intermediate_representation->BeginScopedWriteAccess(
/*final_msaa_count=*/1, surface_props, &begin_semaphores,
&end_semaphores,
gpu::SharedImageRepresentation::AllowUnclearedAccess::kYes);
absl::optional<SkVector> scaling;
if (request->is_scaled()) {
@ -1004,21 +1024,22 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
request->scale_from().y());
}
scoped_write->surface()->wait(begin_semaphores.size(),
begin_semaphores.data());
intermediate_scoped_write->surface()->wait(begin_semaphores.size(),
begin_semaphores.data());
RenderSurface(surface, src_rect, scaling,
is_downscale_or_identity_in_both_dimensions,
scoped_write->surface());
intermediate_scoped_write->surface());
if (request->has_blit_request()) {
BlendBitmapOverlays(scoped_write->surface()->getCanvas(),
BlendBitmapOverlays(intermediate_scoped_write->surface()->getCanvas(),
request->blit_request());
}
auto source_image = scoped_write->surface()->makeImageSnapshot();
if (!source_image) {
DLOG(ERROR) << "failed to retrieve source_image.";
auto intermediate_image =
intermediate_scoped_write->surface()->makeImageSnapshot();
if (!intermediate_image) {
DLOG(ERROR) << "failed to retrieve `intermediate_image`.";
return;
}
@ -1030,19 +1051,27 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
plane_access_datas[1].scoped_write->surface(), nullptr, nullptr};
// The region to be populated in caller's textures is derived from blit
// request's |destination_region_offset|, and from COR's |result_selection|.
// If we have a blit request, use it. Otherwise, use an
// request's |destination_region_offset()|, and from COR's
// |result_selection()|. If we have a blit request, use it. Otherwise, use an
// empty rect (which means that entire image will be used as the target of the
// blit - this will not result in rescaling since w/o blit request present,
// the image size matches the |result_selection|).
SkRect dst_region =
// the destination image size matches the |geometry.result_selection|).
const SkRect dst_region =
request->has_blit_request()
? gfx::RectToSkRect(
gfx::Rect(request->blit_request().destination_region_offset(),
destination_size))
intermediate_dst_size))
: SkRect::MakeEmpty();
skia::BlitRGBAToYUVA(source_image.get(), plane_surfaces.data(), yuva_info,
dst_region);
// We should clear destination if BlitRequest asked to letterbox everything
// outside of intended destination region:
const bool clear_destination =
request->has_blit_request()
? request->blit_request().letterboxing_behavior() ==
LetterboxingBehavior::kLetterbox
: false;
skia::BlitRGBAToYUVA(intermediate_image.get(), plane_surfaces.data(),
yuva_info, dst_region, clear_destination);
bool should_submit = false;
@ -1059,12 +1088,12 @@ void SkiaOutputSurfaceImplOnGpu::CopyOutputNV12(
}
}
representation->SetCleared();
intermediate_representation->SetCleared();
should_submit |= !end_semaphores.empty();
if (!FlushSurface(scoped_write->surface(), end_semaphores,
scoped_write->TakeEndState())) {
if (!FlushSurface(intermediate_scoped_write->surface(), end_semaphores,
intermediate_scoped_write->TakeEndState())) {
// TODO(penghuang): handle vulkan device lost.
FailedSkiaFlush("CopyOutputNV12 dest_surface->flush()");
return;

@ -37,6 +37,8 @@
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
using media::VideoCaptureOracle;
@ -111,6 +113,46 @@ CopyOutputRequest::ResultFormat VideoPixelFormatToCopyOutputRequestFormat(
}
}
bool IsCompatibleWithFormat(const gfx::Rect& rect,
media::VideoPixelFormat format) {
DCHECK(format == media::PIXEL_FORMAT_I420 ||
format == media::PIXEL_FORMAT_NV12 ||
format == media::PIXEL_FORMAT_ARGB);
if (format == media::PIXEL_FORMAT_ARGB) {
// No special requirements:
return true;
}
return rect.origin().x() % 2 == 0 && rect.origin().y() % 2 == 0 &&
rect.width() % 2 == 0 && rect.height() % 2 == 0;
}
// Given a |visible_rect| representing visible rectangle of some video frame,
// calculates a centered rectangle that fits entirely within |visible_rect| and
// has the same aspect ratio as |source_size|, taking into account
// |pixel_format|.
gfx::Rect GetContentRectangle(const gfx::Rect& visible_rect,
const gfx::Size& source_size,
media::VideoPixelFormat pixel_format) {
DCHECK(pixel_format == media::PIXEL_FORMAT_I420 ||
pixel_format == media::PIXEL_FORMAT_NV12 ||
pixel_format == media::PIXEL_FORMAT_ARGB);
if (pixel_format == media::PIXEL_FORMAT_I420 ||
pixel_format == media::PIXEL_FORMAT_NV12) {
return media::ComputeLetterboxRegionForI420(visible_rect, source_size);
} else {
DCHECK_EQ(media::PIXEL_FORMAT_ARGB, pixel_format);
const gfx::Rect content_rect =
media::ComputeLetterboxRegion(visible_rect, source_size);
// The media letterboxing computation explicitly allows for off-by-one
// errors due to computation, so we address those here.
return content_rect.ApproximatelyEqual(visible_rect, 1) ? visible_rect
: content_rect;
}
}
} // namespace
// static
@ -308,6 +350,10 @@ void FrameSinkVideoCapturerImpl::SetResolutionConstraints(
bool use_fixed_aspect_ratio) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DVLOG(2) << __func__ << ": min_size=" << min_size.ToString()
<< ", max_size=" << max_size.ToString()
<< ", use_fixed_aspect_ratio=" << use_fixed_aspect_ratio;
TRACE_EVENT_INSTANT2("gpu.capture", "SetResolutionConstraints",
TRACE_EVENT_SCOPE_THREAD, "min_size.width",
min_size.width(), "min_size.height", min_size.height());
@ -695,6 +741,7 @@ void FrameSinkVideoCapturerImpl::MaybeCaptureFrame(
// TODO(https://crbug.com/1300943): we should likely just get the frame
// region from the last aggregated surface.
if (!compositor_frame_region.Contains(capture_region)) {
DVLOG(3) << __func__ << ": skipping capture!";
MaybeScheduleRefreshFrame();
return;
}
@ -707,9 +754,23 @@ void FrameSinkVideoCapturerImpl::MaybeCaptureFrame(
// Reserve a buffer from the pool for the next frame.
const OracleFrameNumber oracle_frame_number = oracle_->next_frame_number();
// Size of the video frames that we are supposed to produce. Depends on the
// pixel format and the capture size as determined by the oracle (which in
// turn depends on the capture constraints).
const gfx::Size capture_size =
AdjustSizeForPixelFormat(oracle_->capture_size());
// Size of the source that we are capturing:
const gfx::Size source_size = oracle_->source_size();
DCHECK_EQ(capture_region.size(), source_size);
DCHECK(!source_size.IsEmpty());
DVLOG(3) << __func__
<< ": compositor_frame_region=" << compositor_frame_region.ToString()
<< ", capture_region=" << capture_region.ToString()
<< ", capture_size=" << capture_size.ToString();
const bool can_resurrect_content = CanResurrectFrame(capture_size);
scoped_refptr<VideoFrame> frame;
if (can_resurrect_content) {
@ -757,6 +818,13 @@ void FrameSinkVideoCapturerImpl::MaybeCaptureFrame(
return;
}
// If frame was resurrected / allocated from the pool, its visible rectangle
// should match what we requested:
DCHECK_EQ(frame->visible_rect().size(), capture_size);
// The pool should return a frame with visible rectangle that is compatible
// with the capture format.
DCHECK(IsCompatibleWithFormat(frame->visible_rect(), pixel_format_));
// Record a trace event if the capture pipeline is redlining, but capture will
// still proceed.
if (utilization >= 1.0) {
@ -800,23 +868,24 @@ void FrameSinkVideoCapturerImpl::MaybeCaptureFrame(
"frame_number", capture_frame_number, "trigger",
VideoCaptureOracle::EventAsString(event));
const gfx::Size& source_size = oracle_->source_size();
DCHECK(!source_size.IsEmpty());
gfx::Rect content_rect;
if (pixel_format_ == media::PIXEL_FORMAT_I420 ||
pixel_format_ == media::PIXEL_FORMAT_NV12) {
content_rect = media::ComputeLetterboxRegionForI420(frame->visible_rect(),
source_size);
} else {
DCHECK_EQ(media::PIXEL_FORMAT_ARGB, pixel_format_);
content_rect =
media::ComputeLetterboxRegion(frame->visible_rect(), source_size);
// The media letterboxing computation explicitly allows for off-by-one
// errors due to computation, so we address those here.
if (content_rect.ApproximatelyEqual(frame->visible_rect(), 1)) {
content_rect = frame->visible_rect();
}
}
// `content_rect` is the region of the `frame` that we would like to populate.
// We know our source is of size `source_size`, and we have
// `frame->visible_rect()` to fill out - find the largest centered rectangle
// that will fit within the frame and maintains the aspect ratio of the
// source.
// TODO(https://crbug.com/1323342): currently, both the frame's visible
// rectangle and source size are controlled by oracle
// (`frame->visible_rect().size() == `capture_size`). Oracle also knows if we
// need to maintain fixed aspect ratio, so it should compute both the
// `capture_size` and `content_rect` for us, thus ensuring that letterboxing
// happens only when it needs to (i.e. when we allocate a frame and know that
// aspect ratio does not have to be maintained, we should use a size that we
// know would not require letterboxing).
const gfx::Rect content_rect =
GetContentRectangle(frame->visible_rect(), source_size, pixel_format_);
DVLOG(3) << __func__ << ": content_rect=" << content_rect.ToString()
<< ", source_size=" << source_size.ToString()
<< ", frame=" << frame->AsHumanReadableString();
// Determine what rectangular region has changed since the last captured
// frame.
@ -900,7 +969,6 @@ void FrameSinkVideoCapturerImpl::MaybeCaptureFrame(
return;
}
DCHECK(capture_region.size() == source_size);
if (absl::holds_alternative<RegionCaptureCropId>(target_->sub_target)) {
const float scale_factor = frame_metadata.device_scale_factor;
metadata.region_capture_rect =
@ -927,7 +995,12 @@ void FrameSinkVideoCapturerImpl::MaybeCaptureFrame(
std::array<gpu::MailboxHolder, 3> mailbox_holders = {
request_properties.frame->mailbox_holder(0),
request_properties.frame->mailbox_holder(1), gpu::MailboxHolder{}};
blit_request = BlitRequest(content_rect.origin(), mailbox_holders);
// TODO(https://crbug.com/775740): change the capturer to only request the
// parts of the frame that have changed whenever possible.
blit_request =
BlitRequest(content_rect.origin(), LetterboxingBehavior::kLetterbox,
mailbox_holders);
// We haven't captured the frame yet, but let's pretend that we did for the
// sake of blend information computation. We will be asking for an entire
@ -1021,6 +1094,7 @@ void FrameSinkVideoCapturerImpl::DidCopyFrame(
scoped_refptr<media::VideoFrame>& frame = properties.frame;
const gfx::Rect& content_rect = properties.content_rect;
if (log_to_webrtc_ && consumer_) {
std::string format = "";
std::string strides = "";
@ -1131,6 +1205,13 @@ void FrameSinkVideoCapturerImpl::DidCopyFrame(
}
if (frame) {
// The result may be smaller than what was requested, if unforeseen
// clamping to the source boundaries occurred by the executor of the
// CopyOutputRequest. However, the result should never contain more than
// what was requested.
DCHECK_LE(result->size().width(), content_rect.width());
DCHECK_LE(result->size().height(), content_rect.height());
if (!frame->HasGpuMemoryBuffer()) {
// For GMB-backed video frames, overlays were already applied by
// CopyOutputRequest API. For in-memory frames, apply overlays here:
@ -1145,15 +1226,21 @@ void FrameSinkVideoCapturerImpl::DidCopyFrame(
}
}
// The result may be smaller than what was requested, if unforeseen
// clamping to the source boundaries occurred by the executor of the
// CopyOutputRequest. However, the result should never contain more than
// what was requested.
DCHECK_LE(result->size().width(), content_rect.width());
DCHECK_LE(result->size().height(), content_rect.height());
media::LetterboxVideoFrame(
frame.get(), gfx::Rect(content_rect.origin(),
AdjustSizeForPixelFormat(result->size())));
const gfx::Rect result_rect =
gfx::Rect(content_rect.origin(), result->size());
DCHECK(IsCompatibleWithFormat(result_rect, pixel_format_));
DVLOG(3) << __func__ << ": result->size()=" << result->size().ToString()
<< ", content_rect=" << content_rect.ToString()
<< ", result_rect=" << result_rect.ToString()
<< ", frame=" << frame->AsHumanReadableString();
if (frame->visible_rect() != result_rect && !frame->HasGpuMemoryBuffer()) {
// If there are parts of the frame that are visible but we have not wrote
// into them, letterbox them. This is not needed for GMB-backed frames as
// the letterboxing happens on GPU.
media::LetterboxVideoFrame(frame.get(), result_rect);
}
if (ShouldMark(*frame, properties.content_version)) {
MarkFrame(frame, properties.content_version);

@ -442,8 +442,9 @@ MATCHER_P2(IsLetterboxedFrame, color, content_rect, "") {
const VideoFrame& frame = *arg;
const gfx::Rect kContentRect = content_rect;
const auto IsLetterboxedPlane = [&frame, kContentRect](int plane,
uint8_t component) {
const auto IsLetterboxedPlane = [&frame, kContentRect, result_listener](
int plane, uint8_t component) {
gfx::Rect content_rect_copy = kContentRect;
if (plane != VideoFrame::kYPlane) {
content_rect_copy = gfx::Rect(
@ -455,10 +456,19 @@ MATCHER_P2(IsLetterboxedFrame, color, content_rect, "") {
for (int col = 0; col < frame.row_bytes(plane); ++col) {
if (content_rect_copy.Contains(gfx::Point(col, row))) {
if (p[col] != component) {
*result_listener << " where pixel at (" << col << ", " << row
<< ") should be inside content rectangle and the "
"component should match 0x"
<< std::hex << component << " but is 0x"
<< std::hex << static_cast<unsigned int>(p[col]);
return false;
}
} else { // Letterbox border around content.
if (plane == VideoFrame::kYPlane && p[col] != 0x00) {
*result_listener << " where pixel at (" << col << ", " << row
<< ") should be outside content rectangle and the "
"component should match 0x00 but is 0x"
<< std::hex << static_cast<unsigned int>(p[col]);
return false;
}
}

@ -84,6 +84,9 @@ MEDIA_EXPORT gfx::Rect ComputeLetterboxRegion(const gfx::Rect& bounds,
// have color distortions around the edges in a letterboxed video frame. Note
// that, in cases where ComputeLetterboxRegion() would return a 1x1-sized Rect,
// this function could return either a 0x0-sized Rect or a 2x2-sized Rect.
// Note that calling this function with `bounds` that already have the aspect
// ratio of `content` is not guaranteed to be a no-op (for context, see
// https://crbug.com/1323367).
MEDIA_EXPORT gfx::Rect ComputeLetterboxRegionForI420(const gfx::Rect& bounds,
const gfx::Size& content);

@ -179,6 +179,8 @@ bool FrameResources::Initialize() {
return false;
}
gpu_memory_buffer_->SetColorSpace(color_space_);
// Bind SharedImages to each plane.
constexpr size_t kNumPlanes = 2;
constexpr gfx::BufferPlane kPlanes[kNumPlanes] = {gfx::BufferPlane::Y,

@ -4,24 +4,45 @@
#include "skia/ext/rgba_to_yuva.h"
#include <array>
#include "base/logging.h"
#include "base/notreached.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkClipOp.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkColorFilter.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/effects/SkColorMatrix.h"
namespace skia {
namespace {
SkRect GetSubsampledRect(const SkRect& rect,
const std::array<float, 2>& subsampling_factors) {
return SkRect::MakeXYWH(rect.x() * subsampling_factors[0],
rect.y() * subsampling_factors[1],
rect.width() * subsampling_factors[0],
rect.height() * subsampling_factors[1]);
}
} // namespace
void BlitRGBAToYUVA(SkImage* src_image,
SkSurface* dst_surfaces[SkYUVAInfo::kMaxPlanes],
const SkYUVAInfo& dst_yuva_info,
const SkRect& dst_region) {
const SkRect& dst_region,
bool clear_destination) {
// Rectangle representing the entire destination image:
const SkRect dst_image_rect = SkRect::Make(dst_yuva_info.dimensions());
const SkRect src_rect = SkRect::Make(src_image->bounds());
const SkRect dst_rect =
dst_region.isEmpty()
? SkRect::MakeSize(SkSize::Make(dst_yuva_info.dimensions()))
: dst_region;
// Region of destination image that is supposed to be populated:
const SkRect dst_rect = dst_region.isEmpty() ? dst_image_rect : dst_region;
DCHECK(dst_image_rect.contains(dst_rect));
// Permutation matrices to select the appropriate YUVA channels for each
// output plane.
@ -50,6 +71,8 @@ void BlitRGBAToYUVA(SkImage* src_image,
// Blit each plane.
for (int plane = 0; plane < dst_yuva_info.numPlanes(); ++plane) {
SkCanvas* plane_canvas = dst_surfaces[plane]->getCanvas();
SkColorMatrix color_matrix = rgb_to_yuv_matrix;
color_matrix.postConcat(permutation_matrices[plane]);
@ -67,21 +90,29 @@ void BlitRGBAToYUVA(SkImage* src_image,
// Subsampling factors are determined by the ratios of the entire image's
// width & height to the dimensions of the passed in surfaces (which should
// also span the entire logical image):
float subsampling_factors[2] = {
std::array<float, 2> subsampling_factors = {
static_cast<float>(dst_surfaces[plane]->width()) /
dst_yuva_info.dimensions().width(),
static_cast<float>(dst_surfaces[plane]->height()) /
dst_yuva_info.dimensions().height(),
};
SkRect plane_dst_rect =
SkRect::MakeXYWH(dst_rect.x() * subsampling_factors[0],
dst_rect.y() * subsampling_factors[1],
dst_rect.width() * subsampling_factors[0],
dst_rect.height() * subsampling_factors[1]);
dst_surfaces[plane]->getCanvas()->drawImageRect(
src_image, src_rect, plane_dst_rect, sampling_options, &paint,
SkCanvas::kFast_SrcRectConstraint);
if (clear_destination && dst_image_rect != dst_rect) {
// If we were told to clear the destination prior to blitting and we know
// the blit won't populate the entire destination image, issue the draw
// call that fills the destination with black and takes into account the
// color conversion needed.
SkPaint clear_paint(paint);
clear_paint.setColor(SK_ColorBLACK);
plane_canvas->drawPaint(clear_paint);
}
const SkRect plane_dst_rect =
GetSubsampledRect(dst_rect, subsampling_factors);
plane_canvas->drawImageRect(src_image, src_rect, plane_dst_rect,
sampling_options, &paint,
SkCanvas::kFast_SrcRectConstraint);
}
}

@ -6,6 +6,7 @@
#define SKIA_EXT_RGBA_TO_YUVA_H_
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/core/SkYUVAInfo.h"
@ -16,11 +17,13 @@ namespace skia {
// `dst_yuva_info`. `dst_yuva_info` describes the entire destination image - the
// results of the blit operation will be placed in its subregion, described by
// `dst_region`. If a default-constructed `dst_region` is passed in, the entire
// destination image will be written to.
// destination image will be written to. If `clear_destination` is true, the
// entire destination image will be cleared with black before the blit.
SK_API void BlitRGBAToYUVA(SkImage* src_image,
SkSurface* dst_surfaces[SkYUVAInfo::kMaxPlanes],
const SkYUVAInfo& dst_yuva_info,
const SkRect& dst_region = SkRect::MakeEmpty());
const SkRect& dst_region = SkRect::MakeEmpty(),
bool clear_destination = false);
} // namespace skia