[Impeller] Adds test to verify wide gamut indexed png decompression fix for Skia. (flutter/engine#45399)

fixes https://github.com/flutter/flutter/issues/133013
depends on skia fix:
https://skia-review.googlesource.com/c/skia/+/751696

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
gaaclarke 2023-09-13 09:29:26 -07:00 committed by GitHub
parent 357107d9a8
commit 47da5d4f07
6 changed files with 328 additions and 184 deletions

View File

@ -189,6 +189,8 @@
../../../flutter/lib/ui/compositing/scene_builder_unittests.cc
../../../flutter/lib/ui/fixtures
../../../flutter/lib/ui/hooks_unittests.cc
../../../flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc
../../../flutter/lib/ui/painting/image_decoder_no_gl_unittests.h
../../../flutter/lib/ui/painting/image_decoder_unittests.cc
../../../flutter/lib/ui/painting/image_dispose_unittests.cc
../../../flutter/lib/ui/painting/image_encoding_unittests.cc

View File

@ -233,6 +233,7 @@ if (enable_unittests) {
"fixtures/hello_loop_2.gif",
"fixtures/hello_loop_2.webp",
"fixtures/FontManifest.json",
"fixtures/WideGamutIndexed.png",
"//flutter/third_party/txt/third_party/fonts/Roboto-Medium.ttf",
]
}
@ -262,6 +263,8 @@ if (enable_unittests) {
sources = [
"compositing/scene_builder_unittests.cc",
"hooks_unittests.cc",
"painting/image_decoder_no_gl_unittests.cc",
"painting/image_decoder_no_gl_unittests.h",
"painting/image_dispose_unittests.cc",
"painting/image_encoding_unittests.cc",
"painting/image_generator_registry_unittests.cc",

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,220 @@
// Copyright 2013 The Flutter 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 "flutter/lib/ui/painting/image_decoder_no_gl_unittests.h"
#include "flutter/fml/endianness.h"
namespace flutter {
namespace testing {
// Tests are disabled for fuchsia.
#if defined(OS_FUCHSIA)
#pragma GCC diagnostic ignored "-Wunreachable-code"
#endif
namespace {
bool IsPngWithPLTE(const uint8_t* bytes, size_t size) {
constexpr std::string_view kPngMagic = "\x89PNG\x0d\x0a\x1a\x0a";
constexpr std::string_view kPngPlte = "PLTE";
constexpr uint32_t kLengthBytes = 4;
constexpr uint32_t kTypeBytes = 4;
constexpr uint32_t kCrcBytes = 4;
if (size < kPngMagic.size()) {
return false;
}
if (memcmp(bytes, kPngMagic.data(), kPngMagic.size()) != 0) {
return false;
}
const uint8_t* end = bytes + size;
const uint8_t* loc = bytes + kPngMagic.size();
while (loc + kLengthBytes + kTypeBytes <= end) {
uint32_t chunk_length =
fml::BigEndianToArch(*reinterpret_cast<const uint32_t*>(loc));
if (memcmp(loc + kLengthBytes, kPngPlte.data(), kPngPlte.size()) == 0) {
return true;
}
loc += kLengthBytes + kTypeBytes + chunk_length + kCrcBytes;
}
return false;
}
} // namespace
float HalfToFloat(uint16_t half) {
switch (half) {
case 0x7c00:
return std::numeric_limits<float>::infinity();
case 0xfc00:
return -std::numeric_limits<float>::infinity();
}
bool negative = half >> 15;
uint16_t exponent = (half >> 10) & 0x1f;
uint16_t fraction = half & 0x3ff;
float fExponent = exponent - 15.0f;
float fFraction = static_cast<float>(fraction) / 1024.f;
float pow_value = powf(2.0f, fExponent);
return (negative ? -1.f : 1.f) * pow_value * (1.0f + fFraction);
}
float DecodeBGR10(uint32_t x) {
const float max = 1.25098f;
const float min = -0.752941f;
const float intercept = min;
const float slope = (max - min) / 1024.0f;
return (x * slope) + intercept;
}
sk_sp<SkData> OpenFixtureAsSkData(const char* name) {
auto fixtures_directory =
fml::OpenDirectory(GetFixturesPath(), false, fml::FilePermission::kRead);
if (!fixtures_directory.is_valid()) {
return nullptr;
}
auto fixture_mapping =
fml::FileMapping::CreateReadOnly(fixtures_directory, name);
if (!fixture_mapping) {
return nullptr;
}
SkData::ReleaseProc on_release = [](const void* ptr, void* context) -> void {
delete reinterpret_cast<fml::FileMapping*>(context);
};
auto data = SkData::MakeWithProc(fixture_mapping->GetMapping(),
fixture_mapping->GetSize(), on_release,
fixture_mapping.get());
if (!data) {
return nullptr;
}
// The data is now owned by Skia.
fixture_mapping.release();
return data;
}
TEST(ImageDecoderNoGLTest, ImpellerWideGamutDisplayP3) {
#if defined(OS_FUCHSIA)
GTEST_SKIP() << "Fuchsia can't load the test fixtures.";
#endif
auto data = OpenFixtureAsSkData("DisplayP3Logo.png");
auto image = SkImages::DeferredFromEncodedData(data);
ASSERT_TRUE(image != nullptr);
ASSERT_EQ(SkISize::Make(100, 100), image->dimensions());
ImageGeneratorRegistry registry;
std::shared_ptr<ImageGenerator> generator =
registry.CreateCompatibleGenerator(data);
ASSERT_TRUE(generator);
auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
std::move(generator));
ASSERT_FALSE(
IsPngWithPLTE(descriptor->data()->bytes(), descriptor->data()->size()));
#if IMPELLER_SUPPORTS_RENDERING
std::shared_ptr<impeller::Allocator> allocator =
std::make_shared<impeller::TestImpellerAllocator>();
std::optional<DecompressResult> wide_result =
ImageDecoderImpeller::DecompressTexture(
descriptor.get(), SkISize::Make(100, 100), {100, 100},
/*supports_wide_gamut=*/true, allocator);
ASSERT_TRUE(wide_result.has_value());
ASSERT_EQ(wide_result->image_info.colorType(), kRGBA_F16_SkColorType);
ASSERT_TRUE(wide_result->image_info.colorSpace()->isSRGB());
const SkPixmap& wide_pixmap = wide_result->sk_bitmap->pixmap();
const uint16_t* half_ptr = static_cast<const uint16_t*>(wide_pixmap.addr());
bool found_deep_red = false;
for (int i = 0; i < wide_pixmap.width() * wide_pixmap.height(); ++i) {
float red = HalfToFloat(*half_ptr++);
float green = HalfToFloat(*half_ptr++);
float blue = HalfToFloat(*half_ptr++);
half_ptr++; // alpha
if (fabsf(red - 1.0931f) < 0.01f && fabsf(green - -0.2268f) < 0.01f &&
fabsf(blue - -0.1501f) < 0.01f) {
found_deep_red = true;
break;
}
}
ASSERT_TRUE(found_deep_red);
std::optional<DecompressResult> narrow_result =
ImageDecoderImpeller::DecompressTexture(
descriptor.get(), SkISize::Make(100, 100), {100, 100},
/*supports_wide_gamut=*/false, allocator);
ASSERT_TRUE(narrow_result.has_value());
ASSERT_EQ(narrow_result->image_info.colorType(), kRGBA_8888_SkColorType);
#endif // IMPELLER_SUPPORTS_RENDERING
}
TEST(ImageDecoderNoGLTest, ImpellerWideGamutIndexedPng) {
#if defined(OS_FUCHSIA)
GTEST_SKIP() << "Fuchsia can't load the test fixtures.";
#endif
auto data = OpenFixtureAsSkData("WideGamutIndexed.png");
auto image = SkImages::DeferredFromEncodedData(data);
ASSERT_TRUE(image != nullptr);
ASSERT_EQ(SkISize::Make(100, 100), image->dimensions());
ImageGeneratorRegistry registry;
std::shared_ptr<ImageGenerator> generator =
registry.CreateCompatibleGenerator(data);
ASSERT_TRUE(generator);
auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
std::move(generator));
ASSERT_TRUE(
IsPngWithPLTE(descriptor->data()->bytes(), descriptor->data()->size()));
#if IMPELLER_SUPPORTS_RENDERING
std::shared_ptr<impeller::Allocator> allocator =
std::make_shared<impeller::TestImpellerAllocator>();
std::optional<DecompressResult> wide_result =
ImageDecoderImpeller::DecompressTexture(
descriptor.get(), SkISize::Make(100, 100), {100, 100},
/*supports_wide_gamut=*/true, allocator);
ASSERT_EQ(wide_result->image_info.colorType(), kBGR_101010x_XR_SkColorType);
ASSERT_TRUE(wide_result->image_info.colorSpace()->isSRGB());
const SkPixmap& wide_pixmap = wide_result->sk_bitmap->pixmap();
const uint32_t* pixel_ptr = static_cast<const uint32_t*>(wide_pixmap.addr());
bool found_deep_red = false;
for (int i = 0; i < wide_pixmap.width() * wide_pixmap.height(); ++i) {
uint32_t pixel = *pixel_ptr++;
float blue = DecodeBGR10((pixel >> 0) & 0x3ff);
float green = DecodeBGR10((pixel >> 10) & 0x3ff);
float red = DecodeBGR10((pixel >> 20) & 0x3ff);
if (fabsf(red - 1.0931f) < 0.01f && fabsf(green - -0.2268f) < 0.01f &&
fabsf(blue - -0.1501f) < 0.01f) {
found_deep_red = true;
break;
}
}
ASSERT_TRUE(found_deep_red);
std::optional<DecompressResult> narrow_result =
ImageDecoderImpeller::DecompressTexture(
descriptor.get(), SkISize::Make(100, 100), {100, 100},
/*supports_wide_gamut=*/false, allocator);
ASSERT_TRUE(narrow_result.has_value());
ASSERT_EQ(narrow_result->image_info.colorType(), kRGBA_8888_SkColorType);
#endif // IMPELLER_SUPPORTS_RENDERING
}
} // namespace testing
} // namespace flutter

View File

@ -0,0 +1,102 @@
// Copyright 2013 The Flutter 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 "flutter/impeller/core/allocator.h"
#include "flutter/impeller/core/device_buffer.h"
#include "flutter/impeller/core/formats.h"
#include "flutter/impeller/geometry/size.h"
#include "flutter/lib/ui/painting/image_decoder.h"
#include "flutter/lib/ui/painting/image_decoder_impeller.h"
#include "flutter/testing/testing.h"
namespace impeller {
class TestImpellerTexture : public Texture {
public:
explicit TestImpellerTexture(TextureDescriptor desc) : Texture(desc) {}
void SetLabel(std::string_view label) override {}
bool IsValid() const override { return true; }
ISize GetSize() const { return GetTextureDescriptor().size; }
bool OnSetContents(const uint8_t* contents, size_t length, size_t slice) {
return true;
}
bool OnSetContents(std::shared_ptr<const fml::Mapping> mapping,
size_t slice) {
return true;
}
};
class TestImpellerDeviceBuffer : public DeviceBuffer {
public:
explicit TestImpellerDeviceBuffer(DeviceBufferDescriptor desc)
: DeviceBuffer(desc) {
bytes_ = static_cast<uint8_t*>(malloc(desc.size));
}
~TestImpellerDeviceBuffer() { free(bytes_); }
private:
std::shared_ptr<Texture> AsTexture(Allocator& allocator,
const TextureDescriptor& descriptor,
uint16_t row_bytes) const override {
return nullptr;
}
bool SetLabel(const std::string& label) override { return true; }
bool SetLabel(const std::string& label, Range range) override { return true; }
uint8_t* OnGetContents() const override { return bytes_; }
bool OnCopyHostBuffer(const uint8_t* source,
Range source_range,
size_t offset) override {
for (auto i = source_range.offset; i < source_range.length; i++, offset++) {
bytes_[offset] = source[i];
}
return true;
}
uint8_t* bytes_;
};
class TestImpellerAllocator : public impeller::Allocator {
public:
TestImpellerAllocator() {}
~TestImpellerAllocator() = default;
private:
uint16_t MinimumBytesPerRow(PixelFormat format) const override { return 0; }
ISize GetMaxTextureSizeSupported() const override {
return ISize{2048, 2048};
}
std::shared_ptr<DeviceBuffer> OnCreateBuffer(
const DeviceBufferDescriptor& desc) override {
return std::make_shared<TestImpellerDeviceBuffer>(desc);
}
std::shared_ptr<Texture> OnCreateTexture(
const TextureDescriptor& desc) override {
return std::make_shared<TestImpellerTexture>(desc);
}
};
} // namespace impeller
namespace flutter {
namespace testing {
float HalfToFloat(uint16_t half);
float DecodeBGR10(uint32_t x);
sk_sp<SkData> OpenFixtureAsSkData(const char* name);
} // namespace testing
} // namespace flutter

View File

@ -11,6 +11,7 @@
#include "flutter/impeller/renderer/context.h"
#include "flutter/lib/ui/painting/image_decoder.h"
#include "flutter/lib/ui/painting/image_decoder_impeller.h"
#include "flutter/lib/ui/painting/image_decoder_no_gl_unittests.h"
#include "flutter/lib/ui/painting/image_decoder_skia.h"
#include "flutter/lib/ui/painting/multi_frame_codec.h"
#include "flutter/runtime/dart_vm.h"
@ -34,81 +35,6 @@
namespace impeller {
class TestImpellerTexture : public Texture {
public:
explicit TestImpellerTexture(TextureDescriptor desc) : Texture(desc) {}
void SetLabel(std::string_view label) override {}
bool IsValid() const override { return true; }
ISize GetSize() const { return GetTextureDescriptor().size; }
bool OnSetContents(const uint8_t* contents, size_t length, size_t slice) {
return true;
}
bool OnSetContents(std::shared_ptr<const fml::Mapping> mapping,
size_t slice) {
return true;
}
};
class TestImpellerDeviceBuffer : public DeviceBuffer {
public:
explicit TestImpellerDeviceBuffer(DeviceBufferDescriptor desc)
: DeviceBuffer(desc) {
bytes_ = static_cast<uint8_t*>(malloc(desc.size));
}
~TestImpellerDeviceBuffer() { free(bytes_); }
private:
std::shared_ptr<Texture> AsTexture(Allocator& allocator,
const TextureDescriptor& descriptor,
uint16_t row_bytes) const override {
return nullptr;
}
bool SetLabel(const std::string& label) override { return true; }
bool SetLabel(const std::string& label, Range range) override { return true; }
uint8_t* OnGetContents() const override { return bytes_; }
bool OnCopyHostBuffer(const uint8_t* source,
Range source_range,
size_t offset) override {
for (auto i = source_range.offset; i < source_range.length; i++, offset++) {
bytes_[offset] = source[i];
}
return true;
}
uint8_t* bytes_;
};
class TestImpellerAllocator : public impeller::Allocator {
public:
TestImpellerAllocator() {}
~TestImpellerAllocator() = default;
private:
uint16_t MinimumBytesPerRow(PixelFormat format) const override { return 0; }
ISize GetMaxTextureSizeSupported() const override {
return ISize{2048, 2048};
}
std::shared_ptr<DeviceBuffer> OnCreateBuffer(
const DeviceBufferDescriptor& desc) override {
return std::make_shared<TestImpellerDeviceBuffer>(desc);
}
std::shared_ptr<Texture> OnCreateTexture(
const TextureDescriptor& desc) override {
return std::make_shared<TestImpellerTexture>(desc);
}
};
class TestImpellerContext : public impeller::Context {
public:
TestImpellerContext() = default;
@ -240,36 +166,6 @@ class TestIOManager final : public IOManager {
FML_DISALLOW_COPY_AND_ASSIGN(TestIOManager);
};
static sk_sp<SkData> OpenFixtureAsSkData(const char* name) {
auto fixtures_directory =
fml::OpenDirectory(GetFixturesPath(), false, fml::FilePermission::kRead);
if (!fixtures_directory.is_valid()) {
return nullptr;
}
auto fixture_mapping =
fml::FileMapping::CreateReadOnly(fixtures_directory, name);
if (!fixture_mapping) {
return nullptr;
}
SkData::ReleaseProc on_release = [](const void* ptr, void* context) -> void {
delete reinterpret_cast<fml::FileMapping*>(context);
};
auto data = SkData::MakeWithProc(fixture_mapping->GetMapping(),
fixture_mapping->GetSize(), on_release,
fixture_mapping.get());
if (!data) {
return nullptr;
}
// The data is now owned by Skia.
fixture_mapping.release();
return data;
}
class ImageDecoderFixtureTest : public FixtureTest {};
TEST_F(ImageDecoderFixtureTest, CanCreateImageDecoder) {
@ -417,24 +313,6 @@ TEST_F(ImageDecoderFixtureTest, ValidImageResultsInSuccess) {
latch.Wait();
}
namespace {
float HalfToFloat(uint16_t half) {
switch (half) {
case 0x7c00:
return std::numeric_limits<float>::infinity();
case 0xfc00:
return -std::numeric_limits<float>::infinity();
}
bool negative = half >> 15;
uint16_t exponent = (half >> 10) & 0x1f;
uint16_t fraction = half & 0x3ff;
float fExponent = exponent - 15.0f;
float fFraction = static_cast<float>(fraction) / 1024.f;
float pow_value = powf(2.0f, fExponent);
return (negative ? -1.f : 1.f) * pow_value * (1.0f + fFraction);
}
} // namespace
TEST_F(ImageDecoderFixtureTest, ImpellerUploadToSharedNoGpu) {
#if !IMPELLER_SUPPORTS_RENDERING
GTEST_SKIP() << "Impeller only test.";
@ -491,57 +369,6 @@ TEST_F(ImageDecoderFixtureTest, ImpellerNullColorspace) {
#endif // IMPELLER_SUPPORTS_RENDERING
}
TEST_F(ImageDecoderFixtureTest, ImpellerWideGamutDisplayP3) {
auto data = OpenFixtureAsSkData("DisplayP3Logo.png");
auto image = SkImages::DeferredFromEncodedData(data);
ASSERT_TRUE(image != nullptr);
ASSERT_EQ(SkISize::Make(100, 100), image->dimensions());
ImageGeneratorRegistry registry;
std::shared_ptr<ImageGenerator> generator =
registry.CreateCompatibleGenerator(data);
ASSERT_TRUE(generator);
auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
std::move(generator));
#if IMPELLER_SUPPORTS_RENDERING
std::shared_ptr<impeller::Allocator> allocator =
std::make_shared<impeller::TestImpellerAllocator>();
std::optional<DecompressResult> wide_result =
ImageDecoderImpeller::DecompressTexture(
descriptor.get(), SkISize::Make(100, 100), {100, 100},
/*supports_wide_gamut=*/true, allocator);
ASSERT_TRUE(wide_result.has_value());
ASSERT_EQ(wide_result->image_info.colorType(), kRGBA_F16_SkColorType);
ASSERT_TRUE(wide_result->image_info.colorSpace()->isSRGB());
const SkPixmap& wide_pixmap = wide_result->sk_bitmap->pixmap();
const uint16_t* half_ptr = static_cast<const uint16_t*>(wide_pixmap.addr());
bool found_deep_red = false;
for (int i = 0; i < wide_pixmap.width() * wide_pixmap.height(); ++i) {
float red = HalfToFloat(*half_ptr++);
float green = HalfToFloat(*half_ptr++);
float blue = HalfToFloat(*half_ptr++);
half_ptr++; // alpha
if (fabsf(red - 1.0931f) < 0.01f && fabsf(green - -0.2268f) < 0.01f &&
fabsf(blue - -0.1501f) < 0.01f) {
found_deep_red = true;
break;
}
}
ASSERT_TRUE(found_deep_red);
std::optional<DecompressResult> narrow_result =
ImageDecoderImpeller::DecompressTexture(
descriptor.get(), SkISize::Make(100, 100), {100, 100},
/*supports_wide_gamut=*/false, allocator);
ASSERT_TRUE(narrow_result.has_value());
ASSERT_EQ(narrow_result->image_info.colorType(), kRGBA_8888_SkColorType);
#endif // IMPELLER_SUPPORTS_RENDERING
}
TEST_F(ImageDecoderFixtureTest, ImpellerPixelConversion32F) {
auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_F32_SkColorType,
SkAlphaType::kUnpremul_SkAlphaType);
@ -570,16 +397,6 @@ TEST_F(ImageDecoderFixtureTest, ImpellerPixelConversion32F) {
#endif // IMPELLER_SUPPORTS_RENDERING
}
namespace {
float DecodeBGR10(uint32_t x) {
const float max = 1.25098f;
const float min = -0.752941f;
const float intercept = min;
const float slope = (max - min) / 1024.0f;
return (x * slope) + intercept;
}
} // namespace
TEST_F(ImageDecoderFixtureTest, ImpellerWideGamutDisplayP3Opaque) {
auto data = OpenFixtureAsSkData("DisplayP3Logo.jpg");
auto image = SkImages::DeferredFromEncodedData(data);