| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <tuple> |
| |
| #include "base/command_line.h" |
| #include "base/compiler_specific.h" |
| #include "base/containers/span.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/test/bind.h" |
| #include "base/test/test_switches.h" |
| #include "build/build_config.h" |
| #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" |
| #include "components/viz/common/gpu/context_provider.h" |
| #include "components/viz/common/resources/shared_image_format.h" |
| #include "components/viz/service/display/viz_pixel_test.h" |
| #include "components/viz/service/display_embedder/skia_output_surface_impl.h" |
| #include "components/viz/service/gl/gpu_service_impl.h" |
| #include "components/viz/test/buildflags.h" |
| #include "components/viz/test/gl_scaler_test_util.h" |
| #include "components/viz/test/paths.h" |
| #include "gpu/command_buffer/client/client_shared_image.h" |
| #include "gpu/command_buffer/client/raster_interface.h" |
| #include "gpu/command_buffer/client/shared_image_interface.h" |
| #include "gpu/command_buffer/common/shared_image_usage.h" |
| #include "gpu/command_buffer/common/sync_token.h" |
| #include "gpu/command_buffer/service/shared_context_state.h" |
| #include "gpu/command_buffer/service/shared_image/shared_image_manager.h" |
| #include "gpu/command_buffer/service/shared_image/shared_image_representation.h" |
| #include "gpu/command_buffer/service/skia_utils.h" |
| #include "gpu/config/gpu_finch_features.h" |
| #include "media/base/media_switches.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/khronos/GLES2/gl2ext.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkImageInfo.h" |
| #include "third_party/skia/include/core/SkYUVAPixmaps.h" |
| #include "third_party/skia/include/gpu/ganesh/GrBackendSemaphore.h" |
| #include "ui/gfx/color_space.h" |
| #include "ui/gfx/color_transform.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/skia_span_util.h" |
| |
| namespace viz { |
| namespace { |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| constexpr float kAvgAbsoluteErrorLimit = 8.f; |
| constexpr int kMaxAbsoluteErrorLimit = 32; |
| |
| cc::FuzzyPixelComparator GetDefaultFuzzyPixelComparator() { |
| return cc::FuzzyPixelComparator() |
| .SetErrorPixelsPercentageLimit(100.f) |
| .SetAvgAbsErrorLimit(kAvgAbsoluteErrorLimit) |
| .SetAbsErrorLimit(kMaxAbsoluteErrorLimit); |
| } |
| #endif |
| |
| base::FilePath GetTestFilePath(const base::FilePath::CharType* basename) { |
| base::FilePath test_dir; |
| base::PathService::Get(Paths::DIR_TEST_DATA, &test_dir); |
| return test_dir.Append(base::FilePath(basename)); |
| } |
| |
| SharedQuadState* CreateSharedQuadState(AggregatedRenderPass* render_pass, |
| const gfx::Rect& rect) { |
| const gfx::Rect layer_rect = rect; |
| const gfx::Rect visible_layer_rect = rect; |
| SharedQuadState* shared_state = render_pass->CreateAndAppendSharedQuadState(); |
| shared_state->SetAll(gfx::Transform(), layer_rect, visible_layer_rect, |
| gfx::MaskFilterInfo(), /*clip=*/std::nullopt, |
| /*contents_opaque=*/false, /*opacity_f=*/1.0f, |
| SkBlendMode::kSrcOver, |
| /*sorting_context=*/0, |
| /*layer_id=*/0u, /*fast_rounded_corner=*/false); |
| return shared_state; |
| } |
| |
| void DeleteSharedImage( |
| scoped_refptr<gpu::ClientSharedImage> client_shared_image, |
| const gpu::SyncToken& sync_token, |
| bool is_lost) { |
| client_shared_image->UpdateDestructionSyncToken(sync_token); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| struct ReadbackTextureInfo { |
| ReadbackTextureInfo(const gfx::Size& size, |
| SkColorType color_type, |
| SkBitmap& out_bitmap) |
| : size(size), color_type(color_type), out_bitmap(out_bitmap) {} |
| |
| gfx::Size size; |
| SkColorType color_type; |
| const raw_ref<SkBitmap> out_bitmap; |
| }; |
| |
| size_t GetRowBytesForColorType(int width, SkColorType color_type) { |
| size_t row_bytes = width; |
| switch (color_type) { |
| case kAlpha_8_SkColorType: |
| break; |
| case kR8G8_unorm_SkColorType: |
| row_bytes *= 2; |
| break; |
| case kRGBA_8888_SkColorType: |
| case kBGRA_8888_SkColorType: |
| row_bytes *= 4; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| return row_bytes; |
| } |
| |
| void ReadbackTexturesOnGpuThread( |
| gpu::SharedImageManager* shared_image_manager, |
| gpu::SharedContextState* context_state, |
| const gpu::Mailbox& mailbox, |
| base::span<ReadbackTextureInfo> readback_texture_infos) { |
| if (!context_state->MakeCurrent(nullptr)) { |
| return; |
| } |
| |
| auto representation = shared_image_manager->ProduceSkia( |
| mailbox, context_state->memory_type_tracker(), context_state); |
| |
| SkSurfaceProps surface_props{0, kUnknown_SkPixelGeometry}; |
| |
| 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); |
| |
| context_state->gr_context()->wait(begin_semaphores.size(), |
| begin_semaphores.data()); |
| |
| for (size_t i = 0; i < readback_texture_infos.size(); i++) { |
| auto* surface = scoped_write->surface(i); |
| |
| auto& texture_size = readback_texture_infos[i].size; |
| auto color_type = readback_texture_infos[i].color_type; |
| auto& out_bitmap = readback_texture_infos[i].out_bitmap.get(); |
| |
| size_t row_bytes = |
| GetRowBytesForColorType(texture_size.width(), color_type); |
| size_t num_bytes = row_bytes * texture_size.height(); |
| |
| DCHECK_EQ(num_bytes, out_bitmap.computeByteSize()); |
| DCHECK_EQ(row_bytes, out_bitmap.rowBytes()); |
| DCHECK_EQ( |
| static_cast<size_t>(out_bitmap.width() * out_bitmap.bytesPerPixel()), |
| out_bitmap.rowBytes()); |
| SkPixmap pixmap( |
| SkImageInfo::Make(texture_size.width(), texture_size.height(), |
| color_type, representation->alpha_type()), |
| out_bitmap.getAddr(0, 0), row_bytes); |
| |
| bool success = surface->readPixels(pixmap, 0, 0); |
| DCHECK(success); |
| } |
| |
| GrFlushInfo flush_info; |
| flush_info.fNumSemaphores = end_semaphores.size(); |
| flush_info.fSignalSemaphores = end_semaphores.data(); |
| |
| gpu::AddVulkanCleanupTaskForSkiaFlush(context_state->vk_context_provider(), |
| &flush_info); |
| |
| // Graphite surfaces do not need to be flushed, only Ganesh ones. |
| if (GrDirectContext* direct_context = context_state->gr_context()) { |
| direct_context->flush(flush_info); |
| } |
| scoped_write->ApplyBackendSurfaceEndState(); |
| } |
| |
| // Reads back NV12 planes from textures returned in the result. |
| // Will issue a task to the GPU thread and block on its completion. |
| // The |texture_size| needs to be passed in explicitly, because if the request |
| // was issued with an appended BlitRequest, the |result->size()| only describes |
| // the size of the region that was populated in the caller-provided textures, |
| // *not* the entire texture. |
| void ReadbackNV12Planes(TestGpuServiceHolder* gpu_service_holder, |
| CopyOutputResult& result, |
| const gfx::Size& texture_size, |
| SkBitmap& out_luma_plane, |
| SkBitmap& out_chroma_planes) { |
| base::WaitableEvent wait; |
| |
| // Some shared image implementations don't allow concurrent read/write to |
| // a same image. At this point, compositor GPU thread might be reading the |
| // image so it's better we issue the readback on the compositor GPU thread to |
| // avoid contention. |
| gpu_service_holder->ScheduleCompositorGpuTask(base::BindLambdaForTesting( |
| [&out_luma_plane, &out_chroma_planes, &result, &wait, &texture_size]() { |
| auto* shared_image_manager = TestGpuServiceHolder::GetInstance() |
| ->gpu_service() |
| ->shared_image_manager(); |
| auto* context_state = TestGpuServiceHolder::GetInstance() |
| ->GetCompositorGpuThreadSharedContextState() |
| .get(); |
| |
| std::vector<ReadbackTextureInfo> texture_infos; |
| texture_infos.emplace_back(texture_size, kAlpha_8_SkColorType, |
| out_luma_plane); |
| texture_infos.emplace_back( |
| gfx::Size(texture_size.width() / 2, texture_size.height() / 2), |
| kR8G8_unorm_SkColorType, out_chroma_planes); |
| |
| ReadbackTexturesOnGpuThread(shared_image_manager, context_state, |
| result.GetSharedImage()->mailbox(), |
| texture_infos); |
| |
| wait.Signal(); |
| })); |
| |
| wait.Wait(); |
| } |
| |
| // Readback RGBA CopyOutputResult from texture into `out_plane`. |
| void ReadbackResultRGBA(TestGpuServiceHolder* gpu_service_holder, |
| bool is_software, |
| CopyOutputResult& result, |
| const gfx::Size& texture_size, |
| SkBitmap& out_plane) { |
| auto mailbox = result.GetSharedImage()->mailbox(); |
| CHECK(!mailbox.IsZero()); |
| |
| if (is_software) { |
| // Access the memory on test thread since SoftwareRenderer has already |
| // written to it here. |
| auto memory_tracker = std::make_unique<gpu::MemoryTypeTracker>(nullptr); |
| auto representation = gpu_service_holder->gpu_service() |
| ->shared_image_manager() |
| ->ProduceMemory(mailbox, memory_tracker.get()); |
| auto access = representation->BeginScopedReadAccess(); |
| UNSAFE_TODO(memcpy(out_plane.pixmap().writable_addr(), |
| access->pixmap().addr(), |
| out_plane.pixmap().computeByteSize())); |
| return; |
| } |
| |
| base::WaitableEvent wait; |
| |
| // Some shared image implementations don't allow concurrent read/write to |
| // a same image. At this point, compositor GPU thread might be reading the |
| // image so it's better we issue the readback on the compositor GPU thread to |
| // avoid contention. |
| gpu_service_holder->ScheduleCompositorGpuTask(base::BindLambdaForTesting( |
| [&out_plane, &mailbox, &wait, &texture_size]() { |
| auto* shared_image_manager = TestGpuServiceHolder::GetInstance() |
| ->gpu_service() |
| ->shared_image_manager(); |
| auto* context_state = TestGpuServiceHolder::GetInstance() |
| ->GetCompositorGpuThreadSharedContextState() |
| .get(); |
| |
| std::vector<ReadbackTextureInfo> texture_infos; |
| texture_infos.emplace_back(texture_size, kRGBA_8888_SkColorType, |
| out_plane); |
| |
| ReadbackTexturesOnGpuThread(shared_image_manager, context_state, |
| mailbox, texture_infos); |
| |
| wait.Signal(); |
| })); |
| |
| wait.Wait(); |
| } |
| |
| // Generates a sequence of bytes of specified length, using the given pattern. |
| // The pattern will be repeated in the generated sequence, and does not have to |
| // fit in the |num_bytes| evenly. For example, for byte pattern A B C and length |
| // 7, the result will be: A B C A B C A. |
| std::vector<uint8_t> GeneratePixels(size_t num_bytes, |
| base::span<const uint8_t> pattern) { |
| std::vector<uint8_t> result; |
| result.reserve(num_bytes); |
| |
| while (result.size() < num_bytes) { |
| result.push_back(pattern[result.size() % pattern.size()]); |
| } |
| |
| return result; |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| } // namespace |
| |
| // Superclass providing common functionality to ReadbackPixelTest variants. |
| class ReadbackPixelTest : public VizPixelTest { |
| public: |
| explicit ReadbackPixelTest(RendererType type) : VizPixelTest(type) {} |
| |
| bool ScaleByHalf() const { |
| DCHECK(is_initialized_); |
| return scale_by_half_; |
| } |
| |
| gfx::Size GetSourceSize() const { |
| DCHECK(is_initialized_); |
| return source_size_; |
| } |
| |
| const SkBitmap& GetSourceBitmap() const { |
| DCHECK(is_initialized_); |
| return source_bitmap_; |
| } |
| |
| // Gets the SkBitmap containing expected output corresponding to the |
| // source bitmap. The dimensions depend on whether the issued request will be |
| // scaled or not. |
| const SkBitmap& GetExpectedOutputBitmap() const { |
| DCHECK(is_initialized_); |
| return expected_bitmap_; |
| } |
| |
| // In order to test coordinate calculations the tests will issue copy |
| // requests for a small region just to the right and below the center of the |
| // entire source texture/framebuffer. |
| gfx::Rect GetRequestArea() const { |
| DCHECK(is_initialized_); |
| gfx::Rect result(source_size_.width() / 2, source_size_.height() / 2, |
| source_size_.width() / 4, source_size_.height() / 4); |
| |
| if (scale_by_half_) { |
| return gfx::ScaleToEnclosingRect(result, 0.5f); |
| } |
| |
| return result; |
| } |
| |
| // Force subclasses to override SetUp(). All subclasses should call |
| // `SetUpReadbackPixeltest()` from within their `SetUp()` override. |
| void SetUp() override = 0; |
| |
| protected: |
| // Returns filepath for expected output PNG. |
| base::FilePath GetExpectedPath() const { |
| return GetTestFilePath( |
| scale_by_half_ ? FILE_PATH_LITERAL("half_of_one_of_16_color_rects.png") |
| : FILE_PATH_LITERAL("one_of_16_color_rects.png")); |
| } |
| |
| // All subclasses should call it from within their virtual SetUp() method. |
| void SetUpReadbackPixeltest(bool scale_by_half) { |
| DCHECK(!is_initialized_); |
| |
| VizPixelTest::SetUp(); |
| |
| scale_by_half_ = scale_by_half; |
| |
| source_bitmap_ = cc::ReadPNGFile( |
| GetTestFilePath(FILE_PATH_LITERAL("16_color_rects.png"))); |
| ASSERT_FALSE(source_bitmap_.isNull()); |
| source_bitmap_.setImmutable(); |
| |
| expected_bitmap_ = cc::ReadPNGFile(GetExpectedPath()); |
| ASSERT_FALSE(expected_bitmap_.isNull()); |
| expected_bitmap_.setImmutable(); |
| |
| source_size_ = gfx::Size(source_bitmap_.width(), source_bitmap_.height()); |
| |
| is_initialized_ = true; |
| } |
| |
| // Issues a CopyOutputRequest and blocks until it has completed. |
| // The issued request can be configured via a |configure_request| callback. |
| std::unique_ptr<CopyOutputResult> IssueCopyOutputRequestAndRender( |
| CopyOutputRequest::ResultFormat format, |
| CopyOutputRequest::ResultDestination destination, |
| base::OnceCallback<void(CopyOutputRequest&)> configure_request) { |
| const SkBitmap& bitmap = GetSourceBitmap(); |
| |
| std::unique_ptr<CopyOutputResult> result; |
| { |
| auto pass = GenerateRootRenderPass(bitmap); |
| |
| base::RunLoop loop; |
| auto request = std::make_unique<CopyOutputRequest>( |
| format, destination, |
| base::BindOnce( |
| [](std::unique_ptr<CopyOutputResult>* result_out, |
| base::OnceClosure quit_closure, |
| std::unique_ptr<CopyOutputResult> result_from_copier) { |
| *result_out = std::move(result_from_copier); |
| std::move(quit_closure).Run(); |
| }, |
| &result, loop.QuitClosure())); |
| |
| std::move(configure_request).Run(*request); |
| |
| pass->copy_requests.push_back(std::move(request)); |
| |
| AggregatedRenderPassList pass_list; |
| SurfaceDamageRectList surface_damage_rect_list; |
| pass_list.push_back(std::move(pass)); |
| |
| renderer_->DrawFrame( |
| &pass_list, 1.0f, gfx::Size(bitmap.width(), bitmap.height()), |
| gfx::DisplayColorSpaces(), std::move(surface_damage_rect_list)); |
| // Call SwapBuffersSkipped(), so the renderer can have a chance to release |
| // resources. |
| renderer_->SwapBuffersSkipped(); |
| |
| loop.Run(); |
| } |
| |
| return result; |
| } |
| |
| scoped_refptr<gpu::ClientSharedImage> CreateSharedImageWithPixels( |
| SharedImageFormat format, |
| gfx::Size size, |
| const gfx::ColorSpace color_space, |
| base::span<const uint8_t> pixels) { |
| auto* sii = child_context_provider_->SharedImageInterface(); |
| CHECK(sii); |
| |
| if (is_software_renderer()) { |
| auto shared_image = sii->CreateSharedImageForSoftwareCompositor( |
| {format, size, color_space, gpu::SHARED_IMAGE_USAGE_CPU_WRITE_ONLY, |
| "TestLabels"}); |
| auto scoped_mapping = shared_image->Map(); |
| UNSAFE_TODO(memcpy(scoped_mapping->GetMemoryForPlane(0).data(), |
| pixels.data(), pixels.size())); |
| return shared_image; |
| } else { |
| return sii->CreateSharedImage( |
| {format, size, color_space, gpu::SHARED_IMAGE_USAGE_DISPLAY_READ, |
| "TestLabels"}, |
| pixels); |
| } |
| } |
| |
| private: |
| // Creates a RenderPass that embeds a single quad containing |bitmap|. |
| std::unique_ptr<AggregatedRenderPass> GenerateRootRenderPass( |
| const SkBitmap& bitmap) { |
| const gfx::Size source_size = gfx::Size(bitmap.width(), bitmap.height()); |
| |
| SharedImageFormat format = |
| (bitmap.info().colorType() == kBGRA_8888_SkColorType) |
| ? SinglePlaneFormat::kBGRA_8888 |
| : SinglePlaneFormat::kRGBA_8888; |
| ResourceId resource_id = CreateSharedImageResource( |
| source_size, format, gfx::SkPixmapToSpan(bitmap.pixmap())); |
| |
| std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map = |
| cc::SendResourceAndGetChildToParentMap( |
| {resource_id}, resource_provider_.get(), |
| child_resource_provider_.get(), |
| child_context_provider_->SharedImageInterface()); |
| ResourceId mapped_resource_id = resource_map[resource_id]; |
| |
| const gfx::Rect output_rect(source_size); |
| auto pass = std::make_unique<AggregatedRenderPass>(); |
| pass->SetNew(AggregatedRenderPassId{1}, output_rect, output_rect, |
| gfx::Transform()); |
| |
| SharedQuadState* sqs = CreateSharedQuadState(pass.get(), output_rect); |
| |
| auto* quad = pass->CreateAndAppendDrawQuad<TileDrawQuad>(); |
| quad->SetNew(sqs, output_rect, output_rect, /*needs_blending=*/false, |
| mapped_resource_id, gfx::RectF(output_rect), |
| /*nearest_neighbor=*/true, |
| /*force_anti_aliasing_off=*/false); |
| return pass; |
| } |
| |
| // Creates a shared image resource containing `bitmap` and returns the |
| // resource id for it. |
| ResourceId CreateSharedImageResource(const gfx::Size& size, |
| SharedImageFormat format, |
| base::span<const uint8_t> pixels) { |
| scoped_refptr<gpu::ClientSharedImage> client_shared_image = |
| CreateSharedImageWithPixels(format, size, gfx::ColorSpace(), pixels); |
| |
| TransferableResource resource = TransferableResource::Make( |
| client_shared_image, TransferableResource::ResourceSource::kTest, |
| client_shared_image->creation_sync_token()); |
| |
| auto release_callback = |
| base::BindOnce(&DeleteSharedImage, std::move(client_shared_image)); |
| return child_resource_provider_->ImportResource( |
| resource, std::move(release_callback)); |
| } |
| |
| bool is_initialized_ = false; |
| |
| gfx::Size source_size_; |
| bool scale_by_half_; |
| SkBitmap source_bitmap_; |
| SkBitmap expected_bitmap_; |
| }; |
| |
| class ReadbackPixelTestRGBA |
| : public ReadbackPixelTest, |
| public testing::WithParamInterface< |
| std::tuple<RendererType, bool, CopyOutputResult::Destination>> { |
| public: |
| ReadbackPixelTestRGBA() |
| : ReadbackPixelTest(std::get<0>(GetParam())), |
| should_scale_by_half_(std::get<1>(GetParam())), |
| request_destination_(std::get<2>(GetParam())) {} |
| |
| CopyOutputResult::Format RequestFormat() const { |
| return CopyOutputResult::Format::RGBA; |
| } |
| |
| CopyOutputResult::Destination RequestDestination() const { |
| return request_destination_; |
| } |
| |
| void SetUp() override { |
| ReadbackPixelTest::SetUpReadbackPixeltest(should_scale_by_half_); |
| } |
| |
| private: |
| bool should_scale_by_half_ = false; |
| CopyOutputResult::Destination request_destination_; |
| }; |
| |
| // Test that RGBA readback works correctly. |
| TEST_P(ReadbackPixelTestRGBA, ExecutesCopyRequest) { |
| // Generates a RenderPass which contains one quad that spans the full output. |
| // The quad has our source image, so the framebuffer should contain the source |
| // image after DrawFrame(). |
| |
| const gfx::Rect result_selection = GetRequestArea(); |
| |
| std::unique_ptr<CopyOutputResult> result = IssueCopyOutputRequestAndRender( |
| RequestFormat(), RequestDestination(), |
| base::BindLambdaForTesting( |
| [this, &result_selection](CopyOutputRequest& request) { |
| // Build CopyOutputRequest based on test parameters. |
| if (ScaleByHalf()) { |
| request.SetUniformScaleRatio(2, 1); |
| } |
| |
| request.set_result_selection(result_selection); |
| })); |
| |
| // Check that a result was produced and is of the expected rect/size. |
| ASSERT_TRUE(result); |
| ASSERT_FALSE(result->IsEmpty()); |
| EXPECT_EQ(result_selection, result->rect()); |
| |
| std::optional<CopyOutputResult::ScopedSkBitmap> scoped_bitmap; |
| SkBitmap actual; |
| |
| // Examine the image in the |result|, and compare it to the baseline PNG file. |
| switch (RequestDestination()) { |
| case CopyOutputResult::Destination::kSystemMemory: { |
| scoped_bitmap = result->ScopedAccessSkBitmap(); |
| actual = scoped_bitmap.value().bitmap(); |
| break; |
| } |
| #if !BUILDFLAG(IS_ANDROID) |
| case CopyOutputResult::Destination::kSharedImage: { |
| const gfx::Size size = result->size(); |
| actual.allocPixels(SkImageInfo::Make(size.width(), size.height(), |
| kRGBA_8888_SkColorType, |
| kUnpremul_SkAlphaType)); |
| ReadbackResultRGBA(gpu_service_holder_, is_software_renderer(), *result, |
| result->size(), actual); |
| break; |
| } |
| #endif |
| default: |
| NOTREACHED(); |
| } |
| |
| base::FilePath expected_path = GetExpectedPath(); |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kRebaselinePixelTests)) { |
| EXPECT_TRUE(cc::WritePNGFile(actual, expected_path, false)); |
| } |
| |
| if (!cc::MatchesPNGFile(actual, expected_path, cc::ExactPixelComparator())) { |
| LOG(ERROR) << "Entire source: " << cc::GetPNGDataUrl(GetSourceBitmap()); |
| ADD_FAILURE(); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| ReadbackPixelTestRGBA, |
| testing::Combine( |
| testing::Values(RendererType::kSkiaGL), |
| // Result scaling: Scale by half? |
| testing::Values(true, false), |
| #if BUILDFLAG(IS_ANDROID) |
| // Exclude NativeTexture for Android test. |
| testing::Values(CopyOutputResult::Destination::kSystemMemory))); |
| #else |
| testing::Values(CopyOutputResult::Destination::kSystemMemory, |
| CopyOutputResult::Destination::kSharedImage))); |
| #endif |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| class ReadbackPixelTestRGBAWithBlit |
| : public ReadbackPixelTest, |
| public testing::WithParamInterface< |
| std::tuple<RendererType, bool, LetterboxingBehavior, bool>> { |
| public: |
| ReadbackPixelTestRGBAWithBlit() |
| : ReadbackPixelTest(std::get<0>(GetParam())), |
| should_scale_by_half_(std::get<1>(GetParam())), |
| letterboxing_behavior_(std::get<2>(GetParam())), |
| populates_gpu_memory_buffer_(std::get<3>(GetParam())) {} |
| |
| CopyOutputResult::Destination RequestDestination() const { |
| return CopyOutputResult::Destination::kSharedImage; |
| } |
| |
| CopyOutputResult::Format RequestFormat() const { |
| return CopyOutputResult::Format::RGBA; |
| } |
| |
| void SetUp() override { |
| ReadbackPixelTest::SetUpReadbackPixeltest(should_scale_by_half_); |
| } |
| |
| LetterboxingBehavior GetLetterboxingBehavior() const { |
| return letterboxing_behavior_; |
| } |
| |
| // Test parameter that will return `true` if we'll claim that the textures we |
| // create come from GpuMemoryBuffer, `false` otherwise. This exercises a |
| // different code path in SkiaRenderer. |
| bool populates_gpu_memory_buffer() const { |
| return populates_gpu_memory_buffer_; |
| } |
| |
| private: |
| bool should_scale_by_half_ = false; |
| LetterboxingBehavior letterboxing_behavior_; |
| bool populates_gpu_memory_buffer_ = false; |
| }; |
| |
| // Test that RGBA readback works correctly using existing textures. |
| TEST_P(ReadbackPixelTestRGBAWithBlit, ExecutesCopyRequestWithBlit) { |
| const gfx::Rect result_selection = GetRequestArea(); |
| |
| // Generate a shared image that will be owned by us. They will be used as the |
| // destination for the issued BlitRequest. The logical size of the image will |
| // be the same as kSourceSize. The destination region will be the same size of |
| // |result_selection| rectangle, with the same center as the center of |
| // kSourceSize rectangle. As a consequence, the CopyOutputResult should |
| // contain the pixels from the source image in the middle, and the rest should |
| // remain unchanged. |
| |
| const gfx::Size source_size = GetSourceSize(); |
| gfx::Rect destination_subregion = gfx::Rect(source_size); |
| destination_subregion.ClampToCenteredSize(result_selection.size()); |
| |
| // RGBA |
| const std::vector<uint8_t> pattern = {255, 0, 0, 255}; |
| const gfx::ColorSpace color_space = gfx::ColorSpace::CreateSRGB(); |
| |
| // Create the dest shared image and pass the pixel data. |
| constexpr auto format = SinglePlaneFormat::kRGBA_8888; |
| std::vector<uint8_t> pixels = |
| GeneratePixels(format.EstimatedSizeInBytes(source_size), pattern); |
| scoped_refptr<gpu::ClientSharedImage> shared_image = |
| CreateSharedImageWithPixels(format, source_size, color_space, pixels); |
| ASSERT_TRUE(shared_image); |
| |
| std::unique_ptr<CopyOutputResult> result = IssueCopyOutputRequestAndRender( |
| RequestFormat(), RequestDestination(), |
| base::BindLambdaForTesting( |
| // Take `shared_image` by copy to keep alive on main thread. |
| [this, shared_image, &result_selection, |
| &destination_subregion](CopyOutputRequest& request) { |
| // Build CopyOutputRequest based on test parameters. |
| if (ScaleByHalf()) { |
| request.SetUniformScaleRatio(2, 1); |
| } |
| |
| request.set_result_selection(result_selection); |
| |
| request.set_blit_request( |
| BlitRequest(destination_subregion.origin(), |
| GetLetterboxingBehavior(), std::move(shared_image), |
| gpu::SyncToken(), populates_gpu_memory_buffer())); |
| })); |
| |
| // Check that a result was produced and is of the expected rect/size. |
| ASSERT_TRUE(result); |
| ASSERT_FALSE(result->IsEmpty()); |
| ASSERT_EQ(result_selection, result->rect()); |
| ASSERT_EQ(result->destination(), CopyOutputResult::Destination::kSharedImage); |
| |
| // Packed plane sizes. Note that for blit request, the size of the returned |
| // textures is caller-controlled, and we have issued a COR w/ blit request |
| // that is supposed to write to an image of |source_size| size. |
| SkBitmap actual; |
| actual.allocPixels( |
| SkImageInfo::Make(source_size.width(), source_size.height(), |
| kRGBA_8888_SkColorType, kPremul_SkAlphaType)); |
| ReadbackResultRGBA(gpu_service_holder_, is_software_renderer(), *result, |
| source_size, actual); |
| |
| // Load the expected subregion from a file - we will then write it on top |
| // of a new, all-red bitmap: |
| SkBitmap expected_subregion = |
| GLScalerTestUtil::CopyAndConvertToRGBA(GetExpectedOutputBitmap()); |
| |
| // The textures that we passed in to BlitRequest contained RGBA plane data for |
| // an all-red image, let's re-create such a bitmap: |
| SkBitmap expected = GLScalerTestUtil::AllocateRGBABitmap(source_size); |
| |
| 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 |
| // loaded from a file into the same subregion of an all-red texture: |
| expected.writePixels(expected_subregion.pixmap(), destination_subregion.x(), |
| destination_subregion.y()); |
| |
| EXPECT_TRUE( |
| cc::MatchesBitmap(actual, expected, GetDefaultFuzzyPixelComparator())); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| ReadbackPixelTestRGBAWithBlit, |
| testing::Combine( |
| testing::Values(RendererType::kSoftware, RendererType::kSkiaGL), |
| testing::Bool(), // Result scaling: Scale by half? |
| testing::Values(LetterboxingBehavior::kDoNotLetterbox, |
| LetterboxingBehavior::kLetterbox), |
| testing::Bool() // Should behave as if COR is populating a GMB? |
| )); |
| |
| // Tests of NV12. |
| |
| // NOTE: These tests don't run on Android. Android doesn't use NV12 GMB and |
| // doesn't support the MultiPlane:: kNV12 format. |
| class ReadbackPixelTestNV12 |
| : public ReadbackPixelTest, |
| public testing::WithParamInterface< |
| std::tuple<RendererType, bool, CopyOutputResult::Destination>> { |
| public: |
| ReadbackPixelTestNV12() |
| : ReadbackPixelTest(std::get<0>(GetParam())), |
| should_scale_by_half_(std::get<1>(GetParam())), |
| request_destination_(std::get<2>(GetParam())) {} |
| |
| CopyOutputResult::Destination RequestDestination() const { |
| return request_destination_; |
| } |
| |
| CopyOutputResult::Format RequestFormat() const { |
| return CopyOutputResult::Format::NV12; |
| } |
| |
| void SetUp() override { |
| ReadbackPixelTest::SetUpReadbackPixeltest(should_scale_by_half_); |
| } |
| |
| private: |
| bool should_scale_by_half_ = false; |
| CopyOutputResult::Destination request_destination_; |
| }; |
| |
| // Test that NV12 readback works correctly. |
| TEST_P(ReadbackPixelTestNV12, ExecutesCopyRequest) { |
| // Generates a RenderPass which contains one quad that spans the full output. |
| // The quad has our source image, so the framebuffer should contain the source |
| // image after DrawFrame(). |
| |
| const gfx::Rect result_selection = GetRequestArea(); |
| |
| // Check if request's width and height are even (required for NV12 format). |
| // The test case expects the result size to match the request size exactly, |
| // which is not possible with NV12 when the request size dimensions aren't |
| // even. |
| ASSERT_TRUE(result_selection.width() % 2 == 0 && |
| result_selection.height() % 2 == 0) |
| << " request size is not even, result_selection.size()=" |
| << result_selection.size().ToString(); |
| |
| // Additionally, the test uses helpers that assume pixel data can be packed (4 |
| // 8-bit values in 1 32-bit pixel). |
| ASSERT_TRUE(result_selection.width() % 4 == 0) |
| << " request width is not divisible by 4, result_selection.width()=" |
| << result_selection.width(); |
| |
| std::unique_ptr<CopyOutputResult> result = IssueCopyOutputRequestAndRender( |
| RequestFormat(), RequestDestination(), |
| base::BindLambdaForTesting( |
| [this, &result_selection](CopyOutputRequest& request) { |
| // Build CopyOutputRequest based on test parameters. |
| if (ScaleByHalf()) { |
| request.SetUniformScaleRatio(2, 1); |
| } |
| |
| request.set_result_selection(result_selection); |
| })); |
| |
| // Check that a result was produced and is of the expected rect/size. |
| ASSERT_TRUE(result); |
| ASSERT_FALSE(result->IsEmpty()); |
| ASSERT_EQ(result_selection, result->rect()); |
| |
| // Examine the image in the |result|, and compare it to the baseline PNG file. |
| // Approach is the same as the one in GLNV12ConverterPixelTest. |
| |
| SkBitmap luma_plane; |
| SkBitmap chroma_planes; |
| |
| // Packed plane sizes: |
| const gfx::Size luma_plane_size = |
| gfx::Size(result->size().width() / 4, result->size().height()); |
| const gfx::Size chroma_planes_size = |
| gfx::Size(luma_plane_size.width(), luma_plane_size.height() / 2); |
| |
| if (RequestDestination() == CopyOutputResult::Destination::kSystemMemory) { |
| // Create a bitmap with packed Y values: |
| luma_plane = GLScalerTestUtil::AllocateRGBABitmap(luma_plane_size); |
| chroma_planes = GLScalerTestUtil::AllocateRGBABitmap(chroma_planes_size); |
| |
| result->ReadNV12Planes(gfx::SkPixmapToWritableSpan(luma_plane.pixmap()), |
| result->size().width(), |
| gfx::SkPixmapToWritableSpan(chroma_planes.pixmap()), |
| result->size().width()); |
| } else { |
| luma_plane = GLScalerTestUtil::AllocateRGBABitmap(luma_plane_size); |
| chroma_planes = GLScalerTestUtil::AllocateRGBABitmap(chroma_planes_size); |
| |
| ReadbackNV12Planes(gpu_service_holder_, *result, result->size(), luma_plane, |
| chroma_planes); |
| } |
| |
| // Allocate new bitmap & populate it with Y & UV data. |
| SkBitmap actual = GLScalerTestUtil::AllocateRGBABitmap(result->size()); |
| actual.eraseColor(SkColorSetARGB(0xff, 0x00, 0x00, 0x00)); |
| |
| GLScalerTestUtil::UnpackPlanarBitmap(luma_plane, 0, &actual); |
| GLScalerTestUtil::UnpackUVBitmap(chroma_planes, &actual); |
| |
| SkBitmap expected = |
| GLScalerTestUtil::CopyAndConvertToRGBA(GetExpectedOutputBitmap()); |
| GLScalerTestUtil::ConvertRGBABitmapToYUV(&expected); |
| |
| EXPECT_TRUE( |
| cc::MatchesBitmap(actual, expected, GetDefaultFuzzyPixelComparator())); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| ReadbackPixelTestNV12, |
| testing::Combine( |
| testing::Values(RendererType::kSkiaGL), |
| // Result scaling: Scale by half? |
| testing::Values(true, false), |
| testing::Values(CopyOutputResult::Destination::kSystemMemory, |
| CopyOutputResult::Destination::kSharedImage))); |
| |
| class ReadbackPixelTestNV12WithBlit |
| : public ReadbackPixelTest, |
| public testing::WithParamInterface< |
| std::tuple<RendererType, bool, LetterboxingBehavior, bool>> { |
| public: |
| ReadbackPixelTestNV12WithBlit() |
| : ReadbackPixelTest(std::get<0>(GetParam())), |
| should_scale_by_half_(std::get<1>(GetParam())), |
| letterboxing_behavior_(std::get<2>(GetParam())), |
| populates_gpu_memory_buffer_(std::get<3>(GetParam())) {} |
| |
| CopyOutputResult::Destination RequestDestination() const { |
| return CopyOutputResult::Destination::kSharedImage; |
| } |
| |
| CopyOutputResult::Format RequestFormat() const { |
| return CopyOutputResult::Format::NV12; |
| } |
| |
| void SetUp() override { |
| ReadbackPixelTest::SetUpReadbackPixeltest(should_scale_by_half_); |
| } |
| |
| LetterboxingBehavior GetLetterboxingBehavior() const { |
| return letterboxing_behavior_; |
| } |
| |
| // Test parameter that will return `true` if we'll claim that the textures we |
| // create come from GpuMemoryBuffer, `false` otherwise. This exercises a |
| // different code path in SkiaRenderer. |
| bool populates_gpu_memory_buffer() const { |
| return populates_gpu_memory_buffer_; |
| } |
| |
| private: |
| bool should_scale_by_half_ = false; |
| LetterboxingBehavior letterboxing_behavior_; |
| bool populates_gpu_memory_buffer_ = false; |
| }; |
| |
| // Test that NV12 readback works correctly using existing textures. |
| TEST_P(ReadbackPixelTestNV12WithBlit, ExecutesCopyRequestWithBlit) { |
| const gfx::Rect result_selection = GetRequestArea(); |
| |
| // Check if request's width and height are even (required for NV12 format). |
| // The test case expects the result size to match the request size exactly, |
| // which is not possible with NV12 when the request size dimensions aren't |
| // even. |
| ASSERT_TRUE(result_selection.width() % 2 == 0 && |
| result_selection.height() % 2 == 0) |
| << " request size is not even, result_selection.size()=" |
| << result_selection.size().ToString(); |
| |
| // Additionally, the test uses helpers that assume pixel data can be packed (4 |
| // 8-bit values in 1 32-bit pixel). |
| ASSERT_TRUE(result_selection.width() % 4 == 0) |
| << " request width is not divisible by 4, result_selection.width()=" |
| << result_selection.width(); |
| |
| // Generate 2 shared images that will be owned by us. They will be used as the |
| // destination for the issued BlitRequest. The logical size of the image will |
| // be the same as kSourceSize. The destination region will be the same size of |
| // |result_selection| rectangle, with the same center as the center of |
| // kSourceSize rectangle. As a consequence, the CopyOutputResult should |
| // contain the pixels from the source image in the middle, and the rest should |
| // remain unchanged. |
| |
| const gfx::Size source_size = GetSourceSize(); |
| gfx::Rect destination_subregion = gfx::Rect(source_size); |
| destination_subregion.ClampToCenteredSize(result_selection.size()); |
| |
| ASSERT_TRUE(destination_subregion.x() % 2 == 0 && |
| destination_subregion.y() % 2 == 0) |
| << " The test case expects the blit region's origin to be even for NV12 " |
| "blit requests"; |
| |
| const SkColor4f yuv_red = {0.245331, 0.399356, 0.939216, 1.0}; |
| |
| const std::vector<uint8_t> luma_pattern = { |
| static_cast<uint8_t>(yuv_red.fR * 255.0f)}; |
| const std::vector<uint8_t> chromas_pattern = { |
| static_cast<uint8_t>(yuv_red.fG * 255.0f), |
| static_cast<uint8_t>(yuv_red.fB * 255.0f)}; |
| |
| auto* sii = child_context_provider_->SharedImageInterface(); |
| auto* ri = child_context_provider_->RasterInterface(); |
| |
| std::array<std::vector<uint8_t>, CopyOutputResult::kNV12MaxPlanes> pixels; |
| std::array<SkPixmap, SkYUVAInfo::kMaxPlanes> pixmaps = {}; |
| for (size_t i = 0; i < CopyOutputResult::kNV12MaxPlanes; ++i) { |
| const gfx::Size plane_size = |
| i == 0 ? source_size |
| : gfx::Size(source_size.width() / 2, source_size.height() / 2); |
| const size_t plane_size_in_bytes = plane_size.GetArea() * (i == 0 ? 1 : 2); |
| |
| pixels[i] = (i == 0) ? GeneratePixels(plane_size_in_bytes, luma_pattern) |
| : GeneratePixels(plane_size_in_bytes, chromas_pattern); |
| |
| auto color_type = i == 0 ? kAlpha_8_SkColorType : kR8G8_unorm_SkColorType; |
| size_t row_bytes = plane_size.width() * (i == 0 ? 1 : 2); |
| pixmaps[i] = |
| SkPixmap(SkImageInfo::Make(plane_size.width(), plane_size.height(), |
| color_type, kUnpremul_SkAlphaType), |
| pixels[i].data(), row_bytes); |
| } |
| |
| auto shared_image = sii->CreateSharedImage( |
| {MultiPlaneFormat::kNV12, source_size, gfx::ColorSpace::CreateREC709(), |
| gpu::SHARED_IMAGE_USAGE_DISPLAY_READ | |
| gpu::SHARED_IMAGE_USAGE_RASTER_WRITE, |
| "TestLabels"}, |
| gpu::kNullSurfaceHandle); |
| CHECK(shared_image); |
| |
| // Create and wait on shared image interface sync token to wait for shared |
| // image creation. |
| std::unique_ptr<gpu::RasterScopedAccess> ri_access = |
| shared_image->BeginRasterAccess(ri, shared_image->creation_sync_token(), |
| /*readonly=*/false); |
| |
| SkYUVAInfo info = |
| SkYUVAInfo({source_size.width(), source_size.height()}, |
| SkYUVAInfo::PlaneConfig::kY_UV, SkYUVAInfo::Subsampling::k420, |
| SkYUVColorSpace::kIdentity_SkYUVColorSpace); |
| SkYUVAPixmaps yuv_pixmap = |
| SkYUVAPixmaps::FromExternalPixmaps(info, pixmaps.data()); |
| ri->WritePixelsYUV(shared_image->mailbox(), yuv_pixmap); |
| |
| // Create and wait on raster interface sync token for write pixels YUV. |
| gpu::SyncToken sync_token = |
| gpu::RasterScopedAccess::EndAccess(std::move(ri_access)); |
| |
| std::unique_ptr<CopyOutputResult> result = IssueCopyOutputRequestAndRender( |
| RequestFormat(), RequestDestination(), |
| base::BindLambdaForTesting([this, &result_selection, |
| &destination_subregion, &shared_image, |
| &sync_token](CopyOutputRequest& request) { |
| // Build CopyOutputRequest based on test parameters. |
| if (ScaleByHalf()) { |
| request.SetUniformScaleRatio(2, 1); |
| } |
| |
| request.set_result_selection(result_selection); |
| |
| request.set_blit_request(BlitRequest( |
| destination_subregion.origin(), GetLetterboxingBehavior(), |
| shared_image, sync_token, populates_gpu_memory_buffer())); |
| })); |
| |
| // Check that a result was produced and is of the expected rect/size. |
| ASSERT_TRUE(result); |
| ASSERT_FALSE(result->IsEmpty()); |
| ASSERT_EQ(result_selection, result->rect()); |
| ASSERT_EQ(result->destination(), CopyOutputResult::Destination::kSharedImage); |
| |
| // Packed plane sizes. Note that for blit request, the size of the returned |
| // textures is caller-controlled, and we have issued a COR w/ blit request |
| // that is supposed to write to an image of |source_size| size. |
| const gfx::Size luma_plane_size = |
| gfx::Size(source_size.width() / 4, source_size.height()); |
| const gfx::Size chroma_planes_size = |
| gfx::Size(luma_plane_size.width(), luma_plane_size.height() / 2); |
| |
| SkBitmap luma_plane = GLScalerTestUtil::AllocateRGBABitmap(luma_plane_size); |
| SkBitmap chroma_planes = |
| GLScalerTestUtil::AllocateRGBABitmap(chroma_planes_size); |
| |
| ReadbackNV12Planes(gpu_service_holder_, *result, source_size, luma_plane, |
| chroma_planes); |
| |
| // Allocate new bitmap & populate it with Y & UV data. |
| SkBitmap actual = GLScalerTestUtil::AllocateRGBABitmap(source_size); |
| actual.eraseColor(SkColorSetARGB(0xff, 0x00, 0x00, 0x00)); |
| |
| GLScalerTestUtil::UnpackPlanarBitmap(luma_plane, 0, &actual); |
| GLScalerTestUtil::UnpackUVBitmap(chroma_planes, &actual); |
| |
| // Load the expected subregion from a file - we will then write it on top of |
| // a new, all-red bitmap: |
| SkBitmap expected_subregion = |
| GLScalerTestUtil::CopyAndConvertToRGBA(GetExpectedOutputBitmap()); |
| |
| // 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); |
| |
| 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 |
| // loaded from a file into the same subregion of an all-red texture: |
| expected.writePixels(expected_subregion.pixmap(), destination_subregion.x(), |
| destination_subregion.y()); |
| |
| // Now let's convert it to YUV so we can compare with the result: |
| GLScalerTestUtil::ConvertRGBABitmapToYUV(&expected); |
| |
| EXPECT_TRUE( |
| cc::MatchesBitmap(actual, expected, GetDefaultFuzzyPixelComparator())); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| ReadbackPixelTestNV12WithBlit, |
| testing::Combine( |
| testing::Values(RendererType::kSkiaGL), |
| testing::Bool(), // Result scaling: Scale by half? |
| testing::Values(LetterboxingBehavior::kDoNotLetterbox, |
| LetterboxingBehavior::kLetterbox), |
| testing::Bool() // Should behave as if COR is populating a GMB? |
| )); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| } // namespace viz |