blob: d6e8be333b5edd2f31beab25fe5ec73315f77a4f [file] [log] [blame]
// Copyright 2015 The Cobalt Authors. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/renderer/render_tree_pixel_tester.h"
#include <algorithm>
#include <memory>
#include <string>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "cobalt/configuration/configuration.h"
#include "cobalt/renderer/backend/render_target.h"
#include "cobalt/renderer/rasterizer/rasterizer.h"
#include "cobalt/renderer/renderer_module.h"
#include "cobalt/renderer/test/png_utils/png_decode.h"
#include "cobalt/renderer/test/png_utils/png_encode.h"
#include "starboard/system.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPixelRef.h"
#include "third_party/skia/include/effects/SkBlurImageFilter.h"
// Avoid overriding of Windows' CreateDirectory macro.
#undef CreateDirectory
using cobalt::render_tree::ResourceProvider;
using cobalt::renderer::test::png_utils::DecodePNGToRGBA;
using cobalt::renderer::test::png_utils::EncodeRGBAToPNG;
namespace cobalt {
namespace renderer {
: gaussian_blur_sigma(6.2f),
output_all_test_details(false) {}
const math::Size& test_surface_dimensions,
const base::FilePath& expected_results_directory,
const base::FilePath& output_directory,
backend::GraphicsContext* graphics_context, const Options& options)
: graphics_context_(graphics_context),
options_(options) {
// Create our offscreen surface that will be the target of our test
// rasterizations.
test_surface_ = graphics_context_->CreateDownloadableOffscreenRenderTarget(
// Create the rasterizer using the platform default RenderModule options.
RendererModule::Options render_module_options;
// Don't purge the Skia font caches on destruction. Doing so will result in
// too much font thrashing during the tests.
render_module_options.purge_skia_font_caches_on_destruction = false;
rasterizer_ = render_module_options.create_rasterizer_function.Run(
graphics_context_, render_module_options);
RenderTreePixelTester::~RenderTreePixelTester() {}
// static
bool RenderTreePixelTester::IsReferencePlatform() {
// The blitter rasterizer often relies on software rendering which may not
// produce the same results as GPU-based rendering.
return false;
const char* rasterizer_type =
const bool is_opengles_rasterizer =
strcmp(rasterizer_type, "hardware") == 0 ||
strcmp(rasterizer_type, "direct-gles") == 0;
char platform_name[32];
return SbSystemGetProperty(kSbSystemPropertyPlatformName, platform_name,
sizeof(platform_name)) &&
strcmp(platform_name, "X11; Linux x86_64") == 0 &&
ResourceProvider* RenderTreePixelTester::GetResourceProvider() const {
return rasterizer_->GetResourceProvider();
namespace {
// Convenience function that will modify a base file path (without an
// extension) to include a postfix and an extension.
// e.g. ModifyBaseFileName("output/tests/my_test", "-expected", "png")
// will return "output/tests/my_test-expected.png".
base::FilePath ModifyBaseFileName(const base::FilePath& base_file_name,
const std::string& postfix,
const std::string& extension) {
return base_file_name.InsertBeforeExtension(postfix).AddExtension(extension);
// This utility function will take a SkBitmap and perform a Gaussian blur on
// it, returning a new, blurred SkBitmap. The sigma parameter defines how
// strong the blur should be.
SkBitmap BlurBitmap(const SkBitmap& bitmap, float sigma) {
SkBitmap premul_alpha_bitmap;
if ( == kN32_SkColorType && == kPremul_SkAlphaType) {
premul_alpha_bitmap = bitmap;
} else {
// We need to convert our image to premultiplied alpha and N32 color
// before proceeding to blur them, as Skia is designed to primarily deal
// only with images in this format.
SkImageInfo premul_alpha_image_info =
SkImageInfo::MakeN32Premul(bitmap.width(), bitmap.height());
bool allocation_successful =
// Since this is a test, just crash.
bitmap.readPixels(premul_alpha_image_info, premul_alpha_bitmap.getPixels(),
premul_alpha_bitmap.rowBytes(), 0, 0);
SkBitmap blurred_bitmap;
bool blurred_bitmap_allocated =
blurred_bitmap.tryAllocN32Pixels(bitmap.width(), bitmap.height());
SkPaint paint;
sk_sp<SkImageFilter> blur_filter(
SkBlurImageFilter::Make(sigma, sigma, nullptr));
SkCanvas canvas(blurred_bitmap);
canvas.clear(SkColorSetARGB(0, 0, 0, 0));
canvas.drawBitmap(premul_alpha_bitmap, 0, 0, &paint);
return blurred_bitmap;
bool BitmapsAreEqual(const SkBitmap& bitmap_a, const SkBitmap& bitmap_b) {
if (bitmap_a.height() != bitmap_b.height() ||
bitmap_a.rowBytes() != bitmap_b.rowBytes()) {
return false;
// Do not need to lock pixels here. See:
void* pixels_a = reinterpret_cast<void*>(bitmap_a.getPixels());
void* pixels_b = reinterpret_cast<void*>(bitmap_b.getPixels());
size_t byte_count = bitmap_a.rowBytes() * bitmap_a.height();
return memcmp(pixels_a, pixels_b, byte_count) == 0;
// Compares bitmap_a with bitmap_b, where the comparison is done by checking
// each pixel to see if any one of its color channel values differ between
// bitmap_a to bitmap_b by more than acceptable_channel_range. If so,
// number_of_diff_pixels is incremented and the corresponding pixel in the
// returned diff SkBitmap is set to white with full alpha. If the pixel color
// channel values are not found to differ significantly, the corresponding
// pixel in the returned diff SkBitmap is set to black with no alpha.
SkBitmap DiffBitmaps(const SkBitmap& bitmap_a, const SkBitmap& bitmap_b,
int acceptable_channel_range, int* number_of_diff_pixels) {
// Construct a diff bitmap where we can place the results of our diff,
// allowing us to mark which pixels differ between bitmap_a and bitmap_b.
SkBitmap bitmap_diff;
bitmap_diff.allocPixels(SkImageInfo::Make(bitmap_a.width(), bitmap_a.height(),
// Initialize the number of pixels that we have found to differ significantly
// from bitmap_a to bitmap_b to 0.
*number_of_diff_pixels = 0;
// Start checking for pixel differences row by row.
for (int r = 0; r < bitmap_a.height(); ++r) {
const uint8_t* pixels_a =
static_cast<uint8_t*>(bitmap_a.pixelRef()->pixels()) +
bitmap_a.rowBytes() * r;
const uint8_t* pixels_b =
static_cast<uint8_t*>(bitmap_b.pixelRef()->pixels()) +
bitmap_b.rowBytes() * r;
// Since the diff image will be set to either all black or all white, we
// reference its pixels with a uint32_t since it simplifies writing to it.
uint32_t* pixels_diff =
static_cast<uint32_t*>(bitmap_diff.pixelRef()->pixels()) +
bitmap_diff.rowBytes() * r / sizeof(uint32_t);
for (int c = 0; c < bitmap_a.width(); ++c) {
// Check each pixel in the current row for differences. We do this by
// looking at each color channel separately and taking the max of the
// differences we see in each channel.
int max_diff = 0;
for (int i = 0; i < 4; ++i) {
max_diff = std::max(max_diff, std::abs(pixels_a[i] - pixels_b[i]));
// If the maximum color channel difference is larger than the acceptable
// error range, we count one diff pixel and adjust the diff SkBitmap
// accordingly.
if (max_diff > acceptable_channel_range) {
// Set the diff image pixel to all white with full alpha.
*pixels_diff = 0xFF0000FF;
} else {
// Set the diff image pixel to all black with no alpha.
*pixels_diff = 0x00000000;
// Get ready for the next pixel in the row.
pixels_a += 4;
pixels_b += 4;
pixels_diff += 1;
return bitmap_diff;
// Helper function to simplify the process of encoding a Skia RGBA8 SkBitmap
// object to a PNG file.
void EncodeSkBitmapToPNG(const base::FilePath& output_file,
const SkBitmap& bitmap) {
DLOG(INFO) << "Writing " << output_file.value();
if ( == kUnpremul_SkAlphaType && == kRGBA_8888_SkColorType) {
// No conversion needed here, simply write out the pixels as is.
bitmap.width(), bitmap.height(), bitmap.rowBytes());
} else {
// First convert the pixels to the proper format and then output them.
SkImageInfo output_image_info =
SkImageInfo::Make(bitmap.width(), bitmap.height(),
kRGBA_8888_SkColorType, kUnpremul_SkAlphaType);
std::unique_ptr<uint8_t[]> pixels(
new uint8_t[output_image_info.minRowBytes() * bitmap.height()]);
// Reformat and copy the pixel data into our fresh pixel buffer.
bitmap.readPixels(output_image_info, pixels.get(),
output_image_info.minRowBytes(), 0, 0);
// Write the pixels out to disk.
EncodeRGBAToPNG(output_file, pixels.get(), bitmap.width(), bitmap.height(),
// Given a chunk of memory formatted as RGBA8 with pitch = width * 4, this
// function will wrap that memory in a SkBitmap that does *not* own the
// pixels and return that.
const SkBitmap CreateBitmapFromRGBAPixels(const math::SizeF& dimensions,
const uint8_t* pixels) {
const int kRGBABytesPerPixel = 4;
SkBitmap bitmap;
SkImageInfo::Make(dimensions.width(), dimensions.height(),
kRGBA_8888_SkColorType, kUnpremul_SkAlphaType),
const_cast<uint8_t*>(pixels), dimensions.width() * kRGBABytesPerPixel);
return bitmap;
bool TestActualAgainstExpectedBitmap(float gaussian_blur_sigma,
float acceptable_channel_range,
const SkBitmap& expected_bitmap,
const SkBitmap& actual_bitmap,
const base::FilePath& output_base_filename,
bool output_failed_test_details,
bool output_all_test_details) {
DCHECK_EQ(kRGBA_8888_SkColorType, expected_bitmap.colorType());
DCHECK_EQ(kRGBA_8888_SkColorType, actual_bitmap.colorType());
// We can try an exact comparison if we don't need to dump out the
// diff and blur images.
bool quick_test_ok = !output_failed_test_details && !output_all_test_details;
if (quick_test_ok && BitmapsAreEqual(expected_bitmap, actual_bitmap)) {
return true;
// We first blur both the actual and expected bitmaps before testing them.
// This is done to permit small 1 or 2 pixel translation differences in the
// images. If these small differences are not acceptable, the blur amount
// specified by gaussian_blur_sigma should be lowered to make the tests
// stricter.
SkBitmap blurred_actual_bitmap =
BlurBitmap(actual_bitmap, gaussian_blur_sigma);
SkBitmap blurred_expected_bitmap =
BlurBitmap(expected_bitmap, gaussian_blur_sigma);
// Diff the blurred actual image with the blurred expected image and
// count how many pixels are out of range.
int number_of_diff_pixels = 0;
SkBitmap diff_image =
DiffBitmaps(blurred_expected_bitmap, blurred_actual_bitmap,
acceptable_channel_range * 255, &number_of_diff_pixels);
// If the user has requested it via command-line flags, we can also output
// the images that were used by this test to help debug any problems.
if (output_all_test_details ||
(number_of_diff_pixels > 0 && output_failed_test_details)) {
ModifyBaseFileName(output_base_filename, "-actual", "png"),
ModifyBaseFileName(output_base_filename, "-expected", "png"),
ModifyBaseFileName(output_base_filename, "-actual-blurred", "png"),
ModifyBaseFileName(output_base_filename, "-expected-blurred", "png"),
ModifyBaseFileName(output_base_filename, "-diff", "png"), diff_image);
// Execute the main check for this rasterizer test: Are all pixels
// in the generated image in the given range of the expected image
// pixels.
return number_of_diff_pixels == 0;
} // namespace
std::unique_ptr<uint8_t[]> RenderTreePixelTester::RasterizeRenderTree(
const scoped_refptr<cobalt::render_tree::Node>& tree) const {
// Rasterize the test render tree to the rasterizer's offscreen render target.
rasterizer::Rasterizer::Options rasterizer_options;
rasterizer_options.flags = rasterizer::Rasterizer::kSubmitFlags_Clear;
rasterizer_->Submit(tree, test_surface_, rasterizer_options);
// Load the texture's pixel data into a CPU memory buffer and return it.
return graphics_context_->DownloadPixelDataAsRGBA(test_surface_);
void RenderTreePixelTester::Rebaseline(
const scoped_refptr<cobalt::render_tree::Node>& test_tree,
const base::FilePath& expected_base_filename) const {
std::unique_ptr<uint8_t[]> test_image_pixels = RasterizeRenderTree(test_tree);
// Wrap the generated image's raw RGBA8 pixel data in a SkBitmap so that
// we can manipulate it using Skia.
const SkBitmap actual_bitmap = CreateBitmapFromRGBAPixels(
test_surface_->GetSize(), test_image_pixels.get());
base::FilePath output_base_filename(
// Create the output directory if it doesn't already exist.
// If the 'rebase' flag is set, we should not run any actual tests but
// instead output the results of our tests so that they can be used as
// expected output for subsequent tests.
ModifyBaseFileName(output_base_filename, "-expected", "png"),
bool RenderTreePixelTester::TestTree(
const scoped_refptr<cobalt::render_tree::Node>& test_tree,
const base::FilePath& expected_base_filename) const {
std::unique_ptr<uint8_t[]> test_image_pixels = RasterizeRenderTree(test_tree);
// Wrap the generated image's raw RGBA8 pixel data in a SkBitmap so that
// we can manipulate it using Skia.
const SkBitmap actual_bitmap = CreateBitmapFromRGBAPixels(
test_surface_->GetSize(), test_image_pixels.get());
// Here we proceed with the the pixel tests. We must first load the
// expected output image from disk and use that to compare against
// the synthesized image.
int expected_width;
int expected_height;
base::FilePath expected_output_file = expected_results_directory_.Append(
ModifyBaseFileName(expected_base_filename, "-expected", "png"));
if (!base::PathExists(expected_output_file)) {
DLOG(WARNING) << "Expected pixel test output file \""
<< expected_output_file.value() << "\" cannot be found.";
// If the expected output file does not exist, we cannot continue, so
// return in failure.
return false;
std::unique_ptr<uint8_t[]> expected_image_pixels =
DecodePNGToRGBA(expected_output_file, &expected_width, &expected_height);
DCHECK_EQ(test_surface_->GetSize().width(), expected_width);
DCHECK_EQ(test_surface_->GetSize().height(), expected_height);
// We then wrap the expected image in a SkBitmap so that we can manipulate
// it with Skia.
const SkBitmap expected_bitmap = CreateBitmapFromRGBAPixels(
test_surface_->GetSize(), expected_image_pixels.get());
// Finally we perform the actual pixel tests on the bitmap given the
// actual and expected bitmaps. If it is requested that test details
// be output, then this function call may have the side effect of generating
// these details as output files.
return TestActualAgainstExpectedBitmap(
options_.gaussian_blur_sigma, options_.acceptable_channel_range,
expected_bitmap, actual_bitmap,
options_.output_failed_test_details, options_.output_all_test_details);
} // namespace renderer
} // namespace cobalt