| // Copyright 2019 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 <stdint.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "build/build_config.h" |
| #include "cc/paint/image_transfer_cache_entry.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkImage.h" |
| #include "third_party/skia/include/core/SkImageInfo.h" |
| #include "third_party/skia/include/core/SkPixmap.h" |
| #include "third_party/skia/include/core/SkRefCnt.h" |
| #include "third_party/skia/include/gpu/GrBackendSurface.h" |
| #include "third_party/skia/include/gpu/GrContext.h" |
| #include "third_party/skia/include/gpu/GrTypes.h" |
| #include "third_party/skia/include/gpu/gl/GrGLInterface.h" |
| #include "third_party/skia/include/gpu/gl/GrGLTypes.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gl/gl_bindings.h" |
| #include "ui/gl/gl_context.h" |
| #include "ui/gl/gl_context_egl.h" |
| #include "ui/gl/gl_share_group.h" |
| #include "ui/gl/gl_surface.h" |
| #include "ui/gl/init/create_gr_gl_interface.h" |
| #include "ui/gl/init/gl_factory.h" |
| |
| namespace cc { |
| namespace { |
| |
| constexpr SkYUVColorSpace kJpegYUVColorSpace = |
| SkYUVColorSpace::kJPEG_SkYUVColorSpace; |
| |
| void MarkTextureAsReleased(SkImage::ReleaseContext context) { |
| auto* released = static_cast<bool*>(context); |
| DCHECK(!*released); |
| *released = true; |
| } |
| |
| // Checks if all the pixels in |image| are |expected_color|. |
| bool CheckImageIsSolidColor(const sk_sp<SkImage>& image, |
| SkColor expected_color) { |
| DCHECK_GE(image->width(), 1); |
| DCHECK_GE(image->height(), 1); |
| SkBitmap bitmap; |
| if (!bitmap.tryAllocN32Pixels(image->width(), image->height())) |
| return false; |
| SkPixmap pixmap; |
| if (!bitmap.peekPixels(&pixmap)) |
| return false; |
| if (!image->readPixels(pixmap, 0 /* srcX */, 0 /* srcY */)) |
| return false; |
| for (int y = 0; y < image->height(); y++) { |
| for (int x = 0; x < image->width(); x++) { |
| if (bitmap.getColor(x, y) != expected_color) |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| class ImageTransferCacheEntryTest |
| : public testing::TestWithParam<YUVDecodeFormat> { |
| public: |
| void SetUp() override { |
| // Initialize a GL GrContext for Skia. |
| surface_ = gl::init::CreateOffscreenGLSurface(gfx::Size()); |
| ASSERT_TRUE(surface_); |
| share_group_ = base::MakeRefCounted<gl::GLShareGroup>(); |
| gl_context_ = base::MakeRefCounted<gl::GLContextEGL>(share_group_.get()); |
| ASSERT_TRUE(gl_context_); |
| ASSERT_TRUE( |
| gl_context_->Initialize(surface_.get(), gl::GLContextAttribs())); |
| ASSERT_TRUE(gl_context_->MakeCurrent(surface_.get())); |
| sk_sp<GrGLInterface> interface(gl::init::CreateGrGLInterface( |
| *gl_context_->GetVersionInfo(), false /* use_version_es2 */)); |
| gr_context_ = GrContext::MakeGL(std::move(interface)); |
| ASSERT_TRUE(gr_context_); |
| } |
| |
| // Creates the textures for a 64x64 YUV 4:2:0 image where all the samples in |
| // all planes are 255u. This corresponds to an RGB color of (255, 121, 255) |
| // assuming the JPEG YUV-to-RGB conversion formulas. Returns a list of |
| // SkImages backed by the textures. Note that the number of textures depends |
| // on the format (obtained using GetParam()). |release_flags| is set to a list |
| // of boolean flags initialized to false. Each flag corresponds to a plane (in |
| // the same order as the returned SkImages). When the texture for that plane |
| // is released by Skia, that flag will be set to true. Returns an empty vector |
| // on failure. |
| std::vector<sk_sp<SkImage>> CreateTestYUVImage( |
| std::unique_ptr<bool[]>* release_flags) { |
| std::vector<sk_sp<SkImage>> plane_images; |
| *release_flags = nullptr; |
| if (GetParam() == YUVDecodeFormat::kYUV3 || |
| GetParam() == YUVDecodeFormat::kYVU3) { |
| *release_flags = |
| std::unique_ptr<bool[]>(new bool[3]{false, false, false}); |
| plane_images = { |
| CreateSolidPlane(gr_context(), 64, 64, GL_R8_EXT, SkColors::kWhite, |
| release_flags->get()), |
| CreateSolidPlane(gr_context(), 32, 32, GL_R8_EXT, SkColors::kWhite, |
| release_flags->get() + 1), |
| CreateSolidPlane(gr_context(), 32, 32, GL_R8_EXT, SkColors::kWhite, |
| release_flags->get() + 2)}; |
| } else if (GetParam() == YUVDecodeFormat::kYUV2) { |
| *release_flags = std::unique_ptr<bool[]>(new bool[2]{false, false}); |
| plane_images = { |
| CreateSolidPlane(gr_context(), 64, 64, GL_R8_EXT, SkColors::kWhite, |
| release_flags->get()), |
| CreateSolidPlane(gr_context(), 32, 32, GL_RG8_EXT, SkColors::kWhite, |
| release_flags->get() + 1)}; |
| } else { |
| NOTREACHED(); |
| return {}; |
| } |
| if (std::all_of(plane_images.cbegin(), plane_images.cend(), |
| [](sk_sp<SkImage> plane) { return !!plane; })) { |
| return plane_images; |
| } |
| return {}; |
| } |
| |
| void DeletePendingTextures() { |
| DCHECK(gr_context_); |
| for (const auto& texture : textures_to_free_) { |
| if (texture.isValid()) |
| gr_context_->deleteBackendTexture(texture); |
| } |
| gr_context_->flush(); |
| textures_to_free_.clear(); |
| } |
| |
| void TearDown() override { |
| DeletePendingTextures(); |
| gr_context_.reset(); |
| surface_->PrepareToDestroy(gl_context_->IsCurrent(surface_.get())); |
| surface_.reset(); |
| gl_context_.reset(); |
| share_group_.reset(); |
| } |
| |
| GrContext* gr_context() const { return gr_context_.get(); } |
| |
| private: |
| // Uploads a texture corresponding to a single plane in a YUV image. All the |
| // samples in the plane are set to |color|. The texture is not owned by Skia: |
| // when Skia doesn't need it anymore, MarkTextureAsReleased() will be called. |
| sk_sp<SkImage> CreateSolidPlane(GrContext* gr_context, |
| int width, |
| int height, |
| GrGLenum texture_format, |
| const SkColor4f& color, |
| bool* released) { |
| GrBackendTexture allocated_texture = gr_context->createBackendTexture( |
| width, height, GrBackendFormat::MakeGL(texture_format, GL_TEXTURE_2D), |
| color, GrMipMapped::kNo, GrRenderable::kNo); |
| if (!allocated_texture.isValid()) |
| return nullptr; |
| textures_to_free_.push_back(allocated_texture); |
| GrGLTextureInfo allocated_texture_info; |
| if (!allocated_texture.getGLTextureInfo(&allocated_texture_info)) |
| return nullptr; |
| DCHECK_EQ(width, allocated_texture.width()); |
| DCHECK_EQ(height, allocated_texture.height()); |
| DCHECK(!allocated_texture.hasMipMaps()); |
| DCHECK(allocated_texture_info.fTarget == GL_TEXTURE_2D); |
| if (texture_format == GL_RG8_EXT) { |
| // TODO(crbug.com/985458): using GL_RGBA8_EXT is a workaround so that we |
| // can make a SkImage out of a GL_RG8_EXT texture. Revisit this once Skia |
| // supports a corresponding SkColorType. |
| allocated_texture = GrBackendTexture( |
| width, height, GrMipMapped::kNo, |
| GrGLTextureInfo{GL_TEXTURE_2D, allocated_texture_info.fID, |
| GL_RGBA8_EXT}); |
| } |
| *released = false; |
| return SkImage::MakeFromTexture( |
| gr_context, allocated_texture, kTopLeft_GrSurfaceOrigin, |
| texture_format == GL_RG8_EXT ? kRGBA_8888_SkColorType |
| : kAlpha_8_SkColorType, |
| kOpaque_SkAlphaType, nullptr /* colorSpace */, MarkTextureAsReleased, |
| released); |
| } |
| |
| std::vector<GrBackendTexture> textures_to_free_; |
| scoped_refptr<gl::GLSurface> surface_; |
| scoped_refptr<gl::GLShareGroup> share_group_; |
| scoped_refptr<gl::GLContext> gl_context_; |
| sk_sp<GrContext> gr_context_; |
| }; |
| |
| TEST_P(ImageTransferCacheEntryTest, HardwareDecodedNoMipsAtCreation) { |
| std::unique_ptr<bool[]> release_flags; |
| std::vector<sk_sp<SkImage>> plane_images = CreateTestYUVImage(&release_flags); |
| ASSERT_EQ(NumberOfPlanesForYUVDecodeFormat(GetParam()), plane_images.size()); |
| |
| // Create a service-side image cache entry backed by these planes and do not |
| // request generating mipmap chains. The |buffer_byte_size| is only used for |
| // accounting, so we just set it to 0u. |
| auto entry(std::make_unique<ServiceImageTransferCacheEntry>()); |
| EXPECT_TRUE(entry->BuildFromHardwareDecodedImage( |
| gr_context(), std::move(plane_images), |
| GetParam() /* plane_images_format */, kJpegYUVColorSpace, |
| 0u /* buffer_byte_size */, false /* needs_mips */)); |
| |
| // We didn't request generating mipmap chains, so the textures we created |
| // above should stay alive until after the cache entry is deleted. |
| EXPECT_TRUE(std::none_of(release_flags.get(), |
| release_flags.get() + plane_images.size(), |
| [](bool released) { return released; })); |
| entry.reset(); |
| EXPECT_TRUE(std::all_of(release_flags.get(), |
| release_flags.get() + plane_images.size(), |
| [](bool released) { return released; })); |
| } |
| |
| TEST_P(ImageTransferCacheEntryTest, HardwareDecodedMipsAtCreation) { |
| #if defined(OS_ANDROID) |
| // TODO(crbug.com/985458): this test is failing on Android for NV12 and we |
| // don't understand why yet. Revisit this once Skia supports an RG8 |
| // SkColorType. |
| if (GetParam() == YUVDecodeFormat::kYUV2) |
| return; |
| #endif |
| std::unique_ptr<bool[]> release_flags; |
| std::vector<sk_sp<SkImage>> plane_images = CreateTestYUVImage(&release_flags); |
| ASSERT_EQ(NumberOfPlanesForYUVDecodeFormat(GetParam()), plane_images.size()); |
| |
| // Create a service-side image cache entry backed by these planes and request |
| // generating mipmap chains at creation time. The |buffer_byte_size| is only |
| // used for accounting, so we just set it to 0u. |
| auto entry(std::make_unique<ServiceImageTransferCacheEntry>()); |
| EXPECT_TRUE(entry->BuildFromHardwareDecodedImage( |
| gr_context(), std::move(plane_images), |
| GetParam() /* plane_images_format */, kJpegYUVColorSpace, |
| 0u /* buffer_byte_size */, true /* needs_mips */)); |
| |
| // We requested generating mipmap chains at creation time, so the textures we |
| // created above should be released by now. |
| EXPECT_TRUE(std::all_of(release_flags.get(), |
| release_flags.get() + plane_images.size(), |
| [](bool released) { return released; })); |
| DeletePendingTextures(); |
| |
| // Make sure that when we read the pixels from the YUV image, we get the |
| // correct RGB color corresponding to the planes created previously. This |
| // basically checks that after deleting the original YUV textures, the new |
| // YUV image is backed by the correct mipped planes. |
| ASSERT_TRUE(entry->image()); |
| EXPECT_TRUE( |
| CheckImageIsSolidColor(entry->image(), SkColorSetRGB(255, 121, 255))); |
| } |
| |
| TEST_P(ImageTransferCacheEntryTest, HardwareDecodedMipsAfterCreation) { |
| #if defined(OS_ANDROID) |
| // TODO(crbug.com/985458): this test is failing on Android for NV12 and we |
| // don't understand why yet. Revisit this once Skia supports an RG8 |
| // SkColorType. |
| if (GetParam() == YUVDecodeFormat::kYUV2) |
| return; |
| #endif |
| std::unique_ptr<bool[]> release_flags; |
| std::vector<sk_sp<SkImage>> plane_images = CreateTestYUVImage(&release_flags); |
| ASSERT_EQ(NumberOfPlanesForYUVDecodeFormat(GetParam()), plane_images.size()); |
| |
| // Create a service-side image cache entry backed by these planes and do not |
| // request generating mipmap chains at creation time. The |buffer_byte_size| |
| // is only used for accounting, so we just set it to 0u. |
| auto entry(std::make_unique<ServiceImageTransferCacheEntry>()); |
| EXPECT_TRUE(entry->BuildFromHardwareDecodedImage( |
| gr_context(), std::move(plane_images), |
| GetParam() /* plane_images_format */, kJpegYUVColorSpace, |
| 0u /* buffer_byte_size */, false /* needs_mips */)); |
| |
| // We didn't request generating mip chains, so the textures we created above |
| // should stay alive for now. |
| EXPECT_TRUE(std::none_of(release_flags.get(), |
| release_flags.get() + plane_images.size(), |
| [](bool released) { return released; })); |
| |
| // Now request generating the mip chains. |
| entry->EnsureMips(); |
| |
| // Now the original textures should have been released. |
| EXPECT_TRUE(std::all_of(release_flags.get(), |
| release_flags.get() + plane_images.size(), |
| [](bool released) { return released; })); |
| DeletePendingTextures(); |
| |
| // Make sure that when we read the pixels from the YUV image, we get the |
| // correct RGB color corresponding to the planes created previously. This |
| // basically checks that after deleting the original YUV textures, the new |
| // YUV image is backed by the correct mipped planes. |
| ASSERT_TRUE(entry->image()); |
| EXPECT_TRUE( |
| CheckImageIsSolidColor(entry->image(), SkColorSetRGB(255, 121, 255))); |
| } |
| |
| std::string TestParamToString( |
| const testing::TestParamInfo<YUVDecodeFormat>& param_info) { |
| switch (param_info.param) { |
| case YUVDecodeFormat::kYUV3: |
| return "YUV3"; |
| case YUVDecodeFormat::kYVU3: |
| return "YVU3"; |
| case YUVDecodeFormat::kYUV2: |
| return "YUV2"; |
| default: |
| NOTREACHED(); |
| return ""; |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| ImageTransferCacheEntryTest, |
| ::testing::Values(YUVDecodeFormat::kYUV3, |
| YUVDecodeFormat::kYVU3, |
| YUVDecodeFormat::kYUV2), |
| TestParamToString); |
| |
| } // namespace |
| } // namespace cc |