| // Copyright 2017 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 <memory> |
| #include <tuple> |
| |
| #include "base/bind.h" |
| #include "base/run_loop.h" |
| #include "cc/test/pixel_test.h" |
| #include "cc/test/pixel_test_utils.h" |
| #include "cc/test/render_pass_test_utils.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/quads/render_pass.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/libyuv/include/libyuv.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| |
| namespace viz { |
| namespace { |
| |
| template <typename RendererType> |
| class CopyOutputScalingPixelTest |
| : public cc::RendererPixelTest<RendererType>, |
| public testing::WithParamInterface< |
| std::tuple<gfx::Vector2d, gfx::Vector2d, CopyOutputResult::Format>> { |
| public: |
| // Include the public accessor method from the parent class. |
| using cc::RendererPixelTest<RendererType>::renderer; |
| |
| void SetUp() override { |
| cc::RendererPixelTest<RendererType>::SetUp(); |
| scale_from_ = std::get<0>(GetParam()); |
| scale_to_ = std::get<1>(GetParam()); |
| result_format_ = std::get<2>(GetParam()); |
| } |
| |
| // This tests that copy requests requesting scaled results execute correctly. |
| // The test procedure creates a scene similar to the wall art that can be |
| // found in the stairwell of a certain Google office building: A white |
| // background" (W=white) and four blocks of different colors (r=red, g=green, |
| // b=blue, y=yellow). |
| // |
| // WWWWWWWWWWWWWWWWWWWWWWWW |
| // WWWWWWWWWWWWWWWWWWWWWWWW |
| // WWWWrrrrWWWWWWWWggggWWWW |
| // WWWWrrrrWWWWWWWWggggWWWW |
| // WWWWWWWWWWWWWWWWWWWWWWWW |
| // WWWWWWWWWWWWWWWWWWWWWWWW |
| // WWWWbbbbWWWWWWWWyyyyWWWW |
| // WWWWbbbbWWWWWWWWyyyyWWWW |
| // WWWWWWWWWWWWWWWWWWWWWWWW |
| // WWWWWWWWWWWWWWWWWWWWWWWW |
| // |
| // The scene is drawn, which also causes the copy request to execute. Then, |
| // the resulting bitmap is compared against an expected bitmap. |
| void RunTest() { |
| const char* result_format_as_str = "<unknown>"; |
| if (result_format_ == CopyOutputResult::Format::RGBA_BITMAP) |
| result_format_as_str = "RGBA_BITMAP"; |
| else if (result_format_ == CopyOutputResult::Format::I420_PLANES) |
| result_format_as_str = "I420_PLANES"; |
| else |
| NOTIMPLEMENTED(); |
| SCOPED_TRACE(testing::Message() |
| << "scale_from=" << scale_from_.ToString() |
| << ", scale_to=" << scale_to_.ToString() |
| << ", result_format=" << result_format_as_str); |
| |
| // These need to be large enough to prevent odd-valued coordinates when |
| // testing I420_PLANES requests. The requests would still work with |
| // odd-valued coordinates, but the pixel comparisons at the end of the test |
| // will fail due to off-by-one chroma reconstruction. That behavior is WAI, |
| // though, since clients making CopyOutputRequests should always align to |
| // even-valued coordinates! |
| constexpr gfx::Size viewport_size = gfx::Size(48, 20); |
| constexpr int x_block = 8; |
| constexpr int y_block = 4; |
| constexpr SkColor smaller_pass_colors[4] = {SK_ColorRED, SK_ColorGREEN, |
| SK_ColorBLUE, SK_ColorYELLOW}; |
| constexpr SkColor root_pass_color = SK_ColorWHITE; |
| |
| RenderPassList list; |
| |
| // Create the render passes drawn on top of the root render pass. |
| RenderPass* smaller_passes[4]; |
| gfx::Rect smaller_pass_rects[4]; |
| int pass_id = 5; |
| for (int i = 0; i < 4; ++i, --pass_id) { |
| smaller_pass_rects[i] = gfx::Rect( |
| i % 2 == 0 ? x_block : (viewport_size.width() - 2 * x_block), |
| i / 2 == 0 ? y_block : (viewport_size.height() - 2 * y_block), |
| x_block, y_block); |
| smaller_passes[i] = |
| AddRenderPass(&list, pass_id, smaller_pass_rects[i], gfx::Transform(), |
| cc::FilterOperations()); |
| cc::AddQuad(smaller_passes[i], smaller_pass_rects[i], |
| smaller_pass_colors[i]); |
| } |
| |
| // Create the root render pass and add all the child passes to it. |
| RenderPass* root_pass = |
| cc::AddRenderPass(&list, pass_id, gfx::Rect(viewport_size), |
| gfx::Transform(), cc::FilterOperations()); |
| for (int i = 0; i < 4; ++i) |
| cc::AddRenderPassQuad(root_pass, smaller_passes[i]); |
| cc::AddQuad(root_pass, gfx::Rect(viewport_size), root_pass_color); |
| |
| renderer()->DecideRenderPassAllocationsForFrame(list); |
| |
| // Make a copy request and execute it by drawing a frame. A subset of the |
| // viewport is requested, to test that scaled offsets are being computed |
| // correctly as well. |
| const gfx::Rect copy_rect(x_block, y_block, |
| viewport_size.width() - 2 * x_block, |
| viewport_size.height() - 2 * y_block); |
| std::unique_ptr<CopyOutputResult> result; |
| { |
| base::RunLoop loop; |
| |
| // Add a dummy copy request to be executed when the RED RenderPass is |
| // drawn (before the root RenderPass). This is a regression test to |
| // confirm GLRenderer state is consistent with the GL context after each |
| // copy request executes, and before the next RenderPass is drawn. |
| // http://crbug.com/792734 |
| bool dummy_ran = false; |
| auto request = std::make_unique<CopyOutputRequest>( |
| result_format_, |
| base::BindOnce( |
| [](bool* dummy_ran, std::unique_ptr<CopyOutputResult> result) { |
| EXPECT_TRUE(!result->IsEmpty()); |
| EXPECT_FALSE(*dummy_ran); |
| *dummy_ran = true; |
| }, |
| &dummy_ran)); |
| // Set a 10X zoom, which should be more than sufficient to disturb the |
| // results of the main copy request (below) if the GL state is not |
| // properly restored. |
| request->SetUniformScaleRatio(1, 10); |
| list.front()->copy_requests.push_back(std::move(request)); |
| |
| // Add a copy request to the root RenderPass, to capture the results of |
| // drawing all passes for this frame. |
| request = std::make_unique<CopyOutputRequest>( |
| result_format_, |
| base::BindOnce( |
| [](bool* dummy_ran, |
| std::unique_ptr<CopyOutputResult>* test_result, |
| const base::Closure& quit_closure, |
| std::unique_ptr<CopyOutputResult> result_from_renderer) { |
| EXPECT_TRUE(*dummy_ran); |
| *test_result = std::move(result_from_renderer); |
| quit_closure.Run(); |
| }, |
| &dummy_ran, &result, loop.QuitClosure())); |
| request->set_result_selection( |
| copy_output::ComputeResultRect(copy_rect, scale_from_, scale_to_)); |
| request->SetScaleRatio(scale_from_, scale_to_); |
| list.back()->copy_requests.push_back(std::move(request)); |
| |
| renderer()->DrawFrame(&list, 1.0f, viewport_size); |
| loop.Run(); |
| } |
| |
| // Check that the result succeeded and provides a bitmap of the expected |
| // size. |
| const gfx::Rect expected_result_rect = |
| copy_output::ComputeResultRect(copy_rect, scale_from_, scale_to_); |
| EXPECT_EQ(expected_result_rect, result->rect()); |
| EXPECT_EQ(result_format_, result->format()); |
| SkBitmap result_bitmap; |
| if (result_format_ == CopyOutputResult::Format::I420_PLANES) |
| result_bitmap = ReadI420ResultToSkBitmap(*result); |
| else |
| result_bitmap = result->AsSkBitmap(); |
| ASSERT_TRUE(result_bitmap.readyToDraw()); |
| ASSERT_EQ(expected_result_rect.width(), result_bitmap.width()); |
| ASSERT_EQ(expected_result_rect.height(), result_bitmap.height()); |
| |
| // Create the "expected result" bitmap. |
| SkBitmap expected_bitmap; |
| expected_bitmap.allocN32Pixels(expected_result_rect.width(), |
| expected_result_rect.height()); |
| expected_bitmap.eraseColor(root_pass_color); |
| for (int i = 0; i < 4; ++i) { |
| gfx::Rect rect = smaller_pass_rects[i] - copy_rect.OffsetFromOrigin(); |
| rect = copy_output::ComputeResultRect(rect, scale_from_, scale_to_); |
| expected_bitmap.erase( |
| smaller_pass_colors[i], |
| SkIRect{rect.x(), rect.y(), rect.right(), rect.bottom()}); |
| } |
| |
| // Do an approximate comparison of the result bitmap to the expected one to |
| // confirm the position and size of the color values in the result is |
| // correct. Allow for pixel values to be a bit off for two reasons: |
| // |
| // 1. The scaler algorithms are not using a naïve box filter, and so will |
| // blend things together at edge boundaries. |
| // 2. In the case of an I420 format request, the chroma is at half- |
| // resolution, and so there can be "fuzzy color blending" at the edges |
| // between the color rects. |
| int num_bad_pixels = 0; |
| gfx::Point first_failure_position; |
| for (int y = 0; y < expected_bitmap.height(); ++y) { |
| for (int x = 0; x < expected_bitmap.width(); ++x) { |
| const SkColor expected = expected_bitmap.getColor(x, y); |
| const SkColor actual = result_bitmap.getColor(x, y); |
| const bool red_bad = |
| (SkColorGetR(expected) < 0x80) != (SkColorGetR(actual) < 0x80); |
| const bool green_bad = |
| (SkColorGetG(expected) < 0x80) != (SkColorGetG(actual) < 0x80); |
| const bool blue_bad = |
| (SkColorGetB(expected) < 0x80) != (SkColorGetB(actual) < 0x80); |
| const bool alpha_bad = |
| (SkColorGetA(expected) < 0x80) != (SkColorGetA(actual) < 0x80); |
| if (red_bad || green_bad || blue_bad || alpha_bad) { |
| if (num_bad_pixels == 0) |
| first_failure_position = gfx::Point(x, y); |
| ++num_bad_pixels; |
| } |
| } |
| } |
| EXPECT_EQ(0, num_bad_pixels) |
| << "First failure position at: " << first_failure_position.ToString() |
| << "\nExpected bitmap: " << cc::GetPNGDataUrl(expected_bitmap) |
| << "\nActual bitmap: " << cc::GetPNGDataUrl(result_bitmap); |
| } |
| |
| private: |
| // Calls result.ReadI420Planes() and then converts the I420 format back to a |
| // SkBitmap for comparison with the expected bitmap. |
| static SkBitmap ReadI420ResultToSkBitmap(const CopyOutputResult& result) { |
| const int result_width = result.rect().width(); |
| const int result_height = result.rect().height(); |
| |
| // Calculate width/height/stride for each plane and allocate temporary |
| // buffers to hold the pixels. Note that the strides for each plane are |
| // being set differently to test that the arguments are correctly plumbed- |
| // through. |
| const int y_width = result_width; |
| const int y_stride = y_width + 7; |
| std::unique_ptr<uint8_t[]> y_data(new uint8_t[y_stride * result_height]); |
| const int chroma_width = (result_width + 1) / 2; |
| const int u_stride = chroma_width + 11; |
| const int v_stride = chroma_width + 17; |
| const int chroma_height = (result_height + 1) / 2; |
| std::unique_ptr<uint8_t[]> u_data(new uint8_t[u_stride * chroma_height]); |
| std::unique_ptr<uint8_t[]> v_data(new uint8_t[v_stride * chroma_height]); |
| |
| // Do the read. |
| const bool success = result.ReadI420Planes( |
| y_data.get(), y_stride, u_data.get(), u_stride, v_data.get(), v_stride); |
| CHECK(success); |
| |
| // Convert to an SkBitmap. |
| SkBitmap bitmap; |
| bitmap.allocPixels(SkImageInfo::Make(result_width, result_height, |
| kBGRA_8888_SkColorType, |
| kPremul_SkAlphaType)); |
| const int error_code = libyuv::I420ToARGB( |
| y_data.get(), y_stride, u_data.get(), u_stride, v_data.get(), v_stride, |
| static_cast<uint8_t*>(bitmap.getPixels()), bitmap.rowBytes(), |
| result_width, result_height); |
| CHECK_EQ(0, error_code); |
| |
| return bitmap; |
| } |
| |
| gfx::Vector2d scale_from_; |
| gfx::Vector2d scale_to_; |
| CopyOutputResult::Format result_format_; |
| }; |
| |
| // Parameters common to all test instantiations. These are tuples consisting of |
| // {scale_from, scale_to, i420_format}. |
| const auto kParameters = |
| testing::Combine(testing::Values(gfx::Vector2d(1, 1), |
| gfx::Vector2d(2, 1), |
| gfx::Vector2d(1, 2), |
| gfx::Vector2d(2, 2)), |
| testing::Values(gfx::Vector2d(1, 1), |
| gfx::Vector2d(2, 1), |
| gfx::Vector2d(1, 2)), |
| testing::Values(CopyOutputResult::Format::RGBA_BITMAP, |
| CopyOutputResult::Format::I420_PLANES)); |
| |
| using GLCopyOutputScalingPixelTest = CopyOutputScalingPixelTest<GLRenderer>; |
| TEST_P(GLCopyOutputScalingPixelTest, ScaledCopyOfDrawnFrame) { |
| RunTest(); |
| } |
| INSTANTIATE_TEST_CASE_P(, GLCopyOutputScalingPixelTest, kParameters); |
| |
| using SoftwareCopyOutputScalingPixelTest = |
| CopyOutputScalingPixelTest<SoftwareRenderer>; |
| TEST_P(SoftwareCopyOutputScalingPixelTest, ScaledCopyOfDrawnFrame) { |
| RunTest(); |
| } |
| INSTANTIATE_TEST_CASE_P(, SoftwareCopyOutputScalingPixelTest, kParameters); |
| |
| } // namespace |
| } // namespace viz |