| // Copyright 2015 Google Inc. 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 |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // 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 <string> |
| |
| #include "base/file_path.h" |
| #include "base/file_util.h" |
| #include "cobalt/renderer/backend/default_graphics_system.h" |
| #include "cobalt/renderer/backend/graphics_context.h" |
| #include "cobalt/renderer/backend/graphics_system.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 "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::backend::GraphicsContext; |
| using cobalt::renderer::backend::GraphicsSystem; |
| using cobalt::renderer::backend::RenderTarget; |
| using cobalt::renderer::test::png_utils::DecodePNGToRGBA; |
| using cobalt::renderer::test::png_utils::EncodeRGBAToPNG; |
| |
| namespace cobalt { |
| namespace renderer { |
| |
| RenderTreePixelTester::Options::Options() |
| : gaussian_blur_sigma(6.2f), |
| acceptable_channel_range(0.08f), |
| output_failed_test_details(false), |
| output_all_test_details(false) {} |
| |
| RenderTreePixelTester::RenderTreePixelTester( |
| const math::Size& test_surface_dimensions, |
| const FilePath& expected_results_directory, |
| const FilePath& output_directory, const Options& options) |
| : expected_results_directory_(expected_results_directory), |
| output_directory_(output_directory), |
| options_(options) { |
| graphics_system_ = cobalt::renderer::backend::CreateDefaultGraphicsSystem(); |
| graphics_context_ = graphics_system_->CreateGraphicsContext(); |
| |
| // Create our offscreen surface that will be the target of our test |
| // rasterizations. |
| test_surface_ = |
| graphics_context_->CreateDownloadableOffscreenRenderTarget( |
| test_surface_dimensions); |
| |
| // 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_.get(), render_module_options); |
| } |
| |
| RenderTreePixelTester::~RenderTreePixelTester() {} |
| |
| 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". |
| FilePath ModifyBaseFileName(const 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 (bitmap.info().colorType() == kN32_SkColorType && |
| bitmap.info().alphaType() == 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 = |
| premul_alpha_bitmap.tryAllocPixels(premul_alpha_image_info); |
| // Since this is a test, just crash. |
| DCHECK(allocation_successful); |
| 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()); |
| DCHECK(blurred_bitmap_allocated); |
| |
| SkPaint paint; |
| sk_sp<SkImageFilter> blur_filter( |
| SkBlurImageFilter::Make(sigma, sigma, nullptr)); |
| paint.setImageFilter(blur_filter); |
| |
| 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: |
| // https://bugs.chromium.org/p/skia/issues/detail?id=6481&desc=2 |
| 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(), |
| kRGBA_8888_SkColorType, |
| kUnpremul_SkAlphaType)); |
| |
| // 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) { |
| ++*number_of_diff_pixels; |
| // 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 FilePath& output_file, const SkBitmap& bitmap) { |
| if (bitmap.info().alphaType() == kUnpremul_SkAlphaType && |
| bitmap.info().colorType() == kRGBA_8888_SkColorType) { |
| // No conversion needed here, simply write out the pixels as is. |
| EncodeRGBAToPNG(output_file, |
| static_cast<uint8_t*>(bitmap.pixelRef()->pixels()), |
| 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); |
| scoped_array<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(), |
| output_image_info.minRowBytes()); |
| } |
| } |
| |
| // 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; |
| bitmap.installPixels( |
| 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 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)) { |
| file_util::CreateDirectory(output_base_filename.DirName()); |
| |
| EncodeSkBitmapToPNG( |
| ModifyBaseFileName(output_base_filename, "-actual", "png"), |
| actual_bitmap); |
| EncodeSkBitmapToPNG( |
| ModifyBaseFileName(output_base_filename, "-expected", "png"), |
| expected_bitmap); |
| EncodeSkBitmapToPNG( |
| ModifyBaseFileName(output_base_filename, "-actual-blurred", "png"), |
| blurred_actual_bitmap); |
| EncodeSkBitmapToPNG( |
| ModifyBaseFileName(output_base_filename, "-expected-blurred", "png"), |
| blurred_expected_bitmap); |
| EncodeSkBitmapToPNG( |
| 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 |
| |
| scoped_array<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 FilePath& expected_base_filename) const { |
| scoped_array<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()); |
| |
| FilePath output_base_filename( |
| output_directory_.Append(expected_base_filename)); |
| |
| // Create the output directory if it doesn't already exist. |
| file_util::CreateDirectory(output_base_filename.DirName()); |
| |
| // 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. |
| EncodeSkBitmapToPNG( |
| ModifyBaseFileName(output_base_filename, "-expected", "png"), |
| actual_bitmap); |
| } |
| |
| bool RenderTreePixelTester::TestTree( |
| const scoped_refptr<cobalt::render_tree::Node>& test_tree, |
| const FilePath& expected_base_filename) const { |
| scoped_array<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; |
| FilePath expected_output_file = expected_results_directory_.Append( |
| ModifyBaseFileName(expected_base_filename, "-expected", "png")); |
| if (!file_util::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; |
| } |
| scoped_array<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, |
| output_directory_.Append(expected_base_filename), |
| options_.output_failed_test_details, options_.output_all_test_details); |
| } |
| |
| } // namespace renderer |
| } // namespace cobalt |