blob: eff1b92e70b8700ea8b3a7e5ef07725bc8dcac6a [file] [log] [blame]
// Copyright 2018 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 "components/viz/common/gl_scaler.h"
#include "build/build_config.h"
#include "cc/test/pixel_test.h"
#include "cc/test/pixel_test_utils.h"
#include "components/viz/common/gl_scaler_test_util.h"
#include "components/viz/common/gpu/context_provider.h"
#include "gpu/GLES2/gl2chromium.h"
#include "gpu/GLES2/gl2extchromium.h"
#include "gpu/command_buffer/client/gles2_implementation.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkRect.h"
namespace viz {
class GLScalerOverscanPixelTest : public cc::PixelTest,
public GLScalerTestUtil {
public:
using Axis = GLScaler::Axis;
using ScalerStage = GLScaler::ScalerStage;
using Shader = GLScaler::Shader;
bool AreMultipleRenderingTargetsSupported() const {
return scaler_->GetMaxDrawBuffersSupported() > 1;
}
// Creates a ScalerStage chain consisting of a single stage having the given
// configuration.
void UseScaler(Shader shader,
Axis primary_axis,
const gfx::Vector2d& scale_from,
const gfx::Vector2d& scale_to) {
scaler_->chain_ = std::make_unique<ScalerStage>(gl_, shader, primary_axis,
scale_from, scale_to);
scaler_->chain_->set_shader_program(scaler_->GetShaderProgram(
shader, GL_UNSIGNED_BYTE, nullptr, GLScaler::Parameters().swizzle));
}
// Converts the given |source_rect| into a possibly-larger one that includes
// all of the pixels that would be sampled by the current scaler (i.e.,
// including overscan). This uses the math of the internal implementation to
// compute the values.
gfx::Rect ToInputRect(gfx::Rect source_rect) {
CHECK(scaler_ && scaler_->chain_);
return scaler_->chain_->ToInputRect(gfx::RectF(source_rect));
}
// Renders images using the current scaler to auto-detect its overscan. This
// does NOT use the internal implementation to compute the values, but instead
// discovers them experimentally. This is used to confirm that: a) the scaler
// behaves as ToInputRect() expects; and b) the math internal to ToInputRect()
// is correct.
//
// The general approach is to upload a source image containing a blue box in
// the center, surrounded by a red background. The size of the blue box is
// varied: It starts out at a size encompassing more than all of the pixels to
// be sampled by the scaler, and is gradually shrunk until the scaler's output
// begins to include red "bleed-in." At that point, the overscan amount is
// confirmed experimentally.
gfx::Vector2d DetectScalerOverscan(const gfx::Vector2d& scale_from,
const gfx::Vector2d& scale_to) {
// Assume a source size three times the "scale from" width and height. This
// allows for scaling the middle third of a source image, to test possible
// bleed-in on all sides of the output.
const gfx::Size src_size(scale_from.x() * 3, scale_from.y() * 3);
// The requested output rect is the center third of the image, in the
// destination coordinate space.
const gfx::Rect dst_rect(
src_size.width() / 3 * scale_to.x() / scale_from.x(),
src_size.height() / 3 * scale_to.y() / scale_from.y(),
src_size.width() / 3 * scale_to.x() / scale_from.x(),
src_size.height() / 3 * scale_to.y() / scale_from.y());
const GLuint dst_texture = texture_helper_->CreateTexture(dst_rect.size());
// This is our "basis for comparison" image. If scaled output images match
// this, then there is no bleed-in.
SkBitmap output_without_bleed_in;
{
const bool did_scale =
scaler_->Scale(texture_helper_->UploadTexture(CreateBlueBoxOnRedImage(
src_size, gfx::Rect(src_size))),
src_size, gfx::Vector2d(0, 0), dst_texture, dst_rect);
CHECK(did_scale);
output_without_bleed_in =
texture_helper_->DownloadTexture(dst_texture, dst_rect.size());
VLOG(2) << scale_from.ToString() << "→" << scale_to.ToString()
<< ": Output without bleed-in is "
<< cc::GetPNGDataUrl(output_without_bleed_in);
}
// Perform a linear search for the minimal overscan values that do not cause
// the red bleed-in in the scaled output image. There are actually two
// separate searches here, one horizontally and one vertically. Note that an
// overscan result of -1 indicates a failed search and/or a broken
// implementation.
gfx::Vector2d min_overscan(5, 5);
for (int is_horizontal = 1; is_horizontal >= 0; --is_horizontal) {
while (min_overscan.x() >= 0 && min_overscan.y() >= 0) {
// Decrease the overscan by one pixel (one dimension at a time).
const gfx::Vector2d overscan(
is_horizontal ? (min_overscan.x() - 1) : min_overscan.x(),
is_horizontal ? min_overscan.y() : (min_overscan.y() - 1));
// Create the source texture consisting of a centered blue box
// surrounded by red.
const gfx::Rect blue_rect(scale_from.x() - overscan.x(),
scale_from.y() - overscan.y(),
scale_from.x() + 2 * overscan.x(),
scale_from.y() + 2 * overscan.y());
const SkBitmap source_image =
CreateBlueBoxOnRedImage(src_size, blue_rect);
const bool did_scale = scaler_->Scale(
texture_helper_->UploadTexture(source_image), src_size,
gfx::Vector2d(0, 0), dst_texture, dst_rect);
CHECK(did_scale);
const SkBitmap output =
texture_helper_->DownloadTexture(dst_texture, dst_rect.size());
// Compare |output| with |output_without_bleed_in|. If they are
// different, then the blue rect became too small.
bool output_has_bleed_in = false;
for (int y = 0; y < output.height(); ++y) {
for (int x = 0; x < output.width(); ++x) {
if (output.getColor(x, y) !=
output_without_bleed_in.getColor(x, y)) {
output_has_bleed_in = true;
break;
}
}
}
VLOG(2) << scale_from.ToString() << "→" << scale_to.ToString()
<< ": Testing overscan=" << overscan.ToString() << std::endl
<< "\tSource image is " << cc::GetPNGDataUrl(source_image)
<< std::endl
<< "\tOutput image is " << cc::GetPNGDataUrl(output);
if (output_has_bleed_in) {
break; // Search complete: Red bleed-in detected.
}
min_overscan = overscan;
}
}
return min_overscan;
}
static SkBitmap CreateBlueBoxOnRedImage(const gfx::Size& size,
const gfx::Rect& blue_rect) {
SkBitmap result = AllocateRGBABitmap(size);
// Note: None of the color channel values should be close to 0 or 255. This
// is because the bicubic scaler will generate values that overshoot and
// clip, and this will mess-up detection of the number of overscan pixels.
result.eraseColor(SkColorSetRGB(0xc0, 0x40, 0x40));
result.erase(SkColorSetRGB(0x40, 0x40, 0xc0),
SkIRect{blue_rect.x(), blue_rect.y(), blue_rect.right(),
blue_rect.bottom()});
return result;
}
protected:
void SetUp() final {
cc::PixelTest::SetUpGLWithoutRenderer(gfx::SurfaceOrigin::kBottomLeft);
scaler_ = std::make_unique<GLScaler>(context_provider());
gl_ = context_provider()->ContextGL();
CHECK(gl_);
texture_helper_ = std::make_unique<GLScalerTestTextureHelper>(gl_);
}
void TearDown() final {
texture_helper_.reset();
gl_ = nullptr;
scaler_.reset();
cc::PixelTest::TearDown();
}
std::unique_ptr<GLScaler> scaler_;
gpu::gles2::GLES2Interface* gl_ = nullptr;
std::unique_ptr<GLScalerTestTextureHelper> texture_helper_;
};
namespace {
constexpr gfx::Rect kTenByTenRect = gfx::Rect(10, 10, 10, 10);
} // namespace
TEST_F(GLScalerOverscanPixelTest, Bilinear) {
constexpr struct {
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
// No scaling.
{gfx::Vector2d(32, 20), gfx::Vector2d(32, 20), gfx::Vector2d(0, 0)},
// Scale by 0.5X.
{gfx::Vector2d(32, 20), gfx::Vector2d(16, 20), gfx::Vector2d(0, 0)},
{gfx::Vector2d(32, 20), gfx::Vector2d(32, 10), gfx::Vector2d(0, 0)},
{gfx::Vector2d(32, 20), gfx::Vector2d(16, 10), gfx::Vector2d(0, 0)},
// Scale by 0.75X.
{gfx::Vector2d(32, 20), gfx::Vector2d(24, 20), gfx::Vector2d(0, 0)},
{gfx::Vector2d(32, 20), gfx::Vector2d(32, 15), gfx::Vector2d(0, 0)},
{gfx::Vector2d(32, 20), gfx::Vector2d(24, 15), gfx::Vector2d(0, 0)},
// Scale by 1.5X.
{gfx::Vector2d(32, 20), gfx::Vector2d(48, 20), gfx::Vector2d(1, 0)},
{gfx::Vector2d(32, 20), gfx::Vector2d(32, 30), gfx::Vector2d(0, 1)},
{gfx::Vector2d(32, 20), gfx::Vector2d(48, 30), gfx::Vector2d(1, 1)},
// Scale by 4X.
{gfx::Vector2d(32, 20), gfx::Vector2d(128, 20), gfx::Vector2d(1, 0)},
{gfx::Vector2d(32, 20), gfx::Vector2d(32, 80), gfx::Vector2d(0, 1)},
{gfx::Vector2d(32, 20), gfx::Vector2d(128, 80), gfx::Vector2d(1, 1)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BILINEAR, Axis::HORIZONTAL, tc.scale_from, tc.scale_to);
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, TwoTapBilinear) {
constexpr struct {
Axis primary_axis;
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
// Scale by 0.25X in one direction only.
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(16, 40),
gfx::Vector2d(0, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(64, 10),
gfx::Vector2d(0, 0)},
// Scale by 0.25X in one direction, 0.5X in the other.
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(16, 20),
gfx::Vector2d(0, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(32, 10),
gfx::Vector2d(0, 0)},
// Scale by 0.75X (1.5X * 0.5X).
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(48, 40),
gfx::Vector2d(1, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(64, 30),
gfx::Vector2d(0, 1)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BILINEAR2, tc.primary_axis, tc.scale_from, tc.scale_to);
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, ThreeTapBilinear) {
constexpr struct {
Axis primary_axis;
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
// Scale by 0.16...X in one direction only.
{Axis::HORIZONTAL, gfx::Vector2d(66, 40), gfx::Vector2d(11, 40),
gfx::Vector2d(0, 0)},
{Axis::VERTICAL, gfx::Vector2d(32, 60), gfx::Vector2d(32, 10),
gfx::Vector2d(0, 0)},
// Scale by 0.16...X in one direction, 0.5X in the other.
{Axis::HORIZONTAL, gfx::Vector2d(66, 40), gfx::Vector2d(11, 20),
gfx::Vector2d(0, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 60), gfx::Vector2d(32, 10),
gfx::Vector2d(0, 0)},
// Scale by 0.75X (3.0X * 0.5X * 0.5X).
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(48, 40),
gfx::Vector2d(1, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(64, 30),
gfx::Vector2d(0, 1)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BILINEAR3, tc.primary_axis, tc.scale_from, tc.scale_to);
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, FourTapBilinear) {
constexpr struct {
Axis primary_axis;
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
// Scale by 0.125X in one direction only.
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(8, 40),
gfx::Vector2d(0, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(64, 5),
gfx::Vector2d(0, 0)},
// Scale by 0.125X in one direction, 0.5X in the other.
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(8, 20),
gfx::Vector2d(0, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(32, 5),
gfx::Vector2d(0, 0)},
// Scale by 0.75X (6.0X * 0.5X * 0.5X * 0.5X).
{Axis::HORIZONTAL, gfx::Vector2d(64, 40), gfx::Vector2d(48, 40),
gfx::Vector2d(1, 0)},
{Axis::VERTICAL, gfx::Vector2d(64, 40), gfx::Vector2d(64, 30),
gfx::Vector2d(0, 1)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BILINEAR4, tc.primary_axis, tc.scale_from, tc.scale_to);
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, TwoByTwoTapBilinear) {
constexpr struct {
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
// Scale by 0.25X in both directions.
{gfx::Vector2d(64, 40), gfx::Vector2d(16, 10), gfx::Vector2d(0, 0)},
// Scale by 0.75X (1.5X * 0.5X) in one direction, 0.25X in the other.
{gfx::Vector2d(64, 40), gfx::Vector2d(48, 10), gfx::Vector2d(1, 0)},
{gfx::Vector2d(64, 40), gfx::Vector2d(16, 30), gfx::Vector2d(0, 1)},
// Scale by 0.75X (1.5X * 0.5X) in both directions.
{gfx::Vector2d(64, 40), gfx::Vector2d(48, 30), gfx::Vector2d(1, 1)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BILINEAR2X2, Axis::HORIZONTAL, tc.scale_from,
tc.scale_to);
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, BicubicUpscale) {
#if defined(OS_ANDROID)
// Unfortunately, on our current Android bots, there are some inaccuracies
// introduced by the platform that seem to throw-off the pixel testing of the
// bicubic sampler.
constexpr bool kSkipDetectionTest = true;
LOG(WARNING) << "Skipping overscan detection due to platform issues.";
#else
constexpr bool kSkipDetectionTest = false;
#endif
constexpr struct {
Axis primary_axis;
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
// Scale by 1.5X, 2X, and 3.3...X horizontally.
{Axis::HORIZONTAL, gfx::Vector2d(12, 10), gfx::Vector2d(18, 10),
gfx::Vector2d(2, 0)},
{Axis::HORIZONTAL, gfx::Vector2d(12, 10), gfx::Vector2d(24, 10),
gfx::Vector2d(2, 0)},
{Axis::HORIZONTAL, gfx::Vector2d(12, 10), gfx::Vector2d(40, 10),
gfx::Vector2d(2, 0)},
// Scale by 1.5X, 2X, and 3.3...X vertically.
{Axis::VERTICAL, gfx::Vector2d(12, 10), gfx::Vector2d(12, 15),
gfx::Vector2d(0, 2)},
{Axis::VERTICAL, gfx::Vector2d(12, 10), gfx::Vector2d(12, 20),
gfx::Vector2d(0, 2)},
{Axis::VERTICAL, gfx::Vector2d(12, 9), gfx::Vector2d(12, 30),
gfx::Vector2d(0, 2)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BICUBIC_UPSCALE, tc.primary_axis, tc.scale_from,
tc.scale_to);
if (!kSkipDetectionTest) {
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
}
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, BicubicHalving) {
constexpr struct {
Axis primary_axis;
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
gfx::Vector2d expected_overscan;
} kTestCases[] = {
{Axis::HORIZONTAL, gfx::Vector2d(16, 16), gfx::Vector2d(8, 16),
gfx::Vector2d(3, 0)},
{Axis::VERTICAL, gfx::Vector2d(16, 16), gfx::Vector2d(16, 8),
gfx::Vector2d(0, 3)},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message() << "scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(Shader::BICUBIC_HALF_1D, tc.primary_axis, tc.scale_from,
tc.scale_to);
EXPECT_EQ(tc.expected_overscan,
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
gfx::Rect expected_input_rect = kTenByTenRect;
expected_input_rect.Inset(-tc.expected_overscan.x(),
-tc.expected_overscan.y());
EXPECT_EQ(expected_input_rect, ToInputRect(kTenByTenRect));
}
}
TEST_F(GLScalerOverscanPixelTest, Planerizers) {
if (!AreMultipleRenderingTargetsSupported()) {
LOG(WARNING) << "Skipping test due to lack of MRT support on this machine.";
return;
}
constexpr struct {
Shader shader;
Axis primary_axis;
gfx::Vector2d scale_from;
gfx::Vector2d scale_to;
} kTestCases[] = {
{Shader::PLANAR_CHANNEL_0, Axis::HORIZONTAL, gfx::Vector2d(16, 16),
gfx::Vector2d(4, 16)},
// Note: Other PLANAR_CHANNEL_N shaders don't need to be tested since they
// use the same code path.
{Shader::I422_NV61_MRT, Axis::HORIZONTAL, gfx::Vector2d(16, 16),
gfx::Vector2d(4, 16)},
{
Shader::DEINTERLEAVE_PAIRWISE_MRT, Axis::HORIZONTAL,
gfx::Vector2d(16, 16), gfx::Vector2d(8, 16),
},
};
for (const auto& tc : kTestCases) {
SCOPED_TRACE(testing::Message()
<< "shader=" << static_cast<int>(tc.shader)
<< ", scale_from=" << tc.scale_from.ToString()
<< ", scale_to=" << tc.scale_to.ToString());
// Test the effect on the pixels.
UseScaler(tc.shader, tc.primary_axis, tc.scale_from, tc.scale_to);
EXPECT_EQ(gfx::Vector2d(0, 0),
DetectScalerOverscan(tc.scale_from, tc.scale_to));
// Sanity-check that the internal math estimating the overscan is correct.
EXPECT_EQ(kTenByTenRect, ToInputRect(kTenByTenRect));
}
}
} // namespace viz