diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 395b977e0ed..9b1f50e0670 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -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 diff --git a/engine/src/flutter/lib/ui/BUILD.gn b/engine/src/flutter/lib/ui/BUILD.gn index 8ad0197bb1d..0db5e123030 100644 --- a/engine/src/flutter/lib/ui/BUILD.gn +++ b/engine/src/flutter/lib/ui/BUILD.gn @@ -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", diff --git a/engine/src/flutter/lib/ui/fixtures/WideGamutIndexed.png b/engine/src/flutter/lib/ui/fixtures/WideGamutIndexed.png new file mode 100644 index 00000000000..b35593c7e89 Binary files /dev/null and b/engine/src/flutter/lib/ui/fixtures/WideGamutIndexed.png differ diff --git a/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc b/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc new file mode 100644 index 00000000000..5ed339303b4 --- /dev/null +++ b/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc @@ -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(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::infinity(); + case 0xfc00: + return -std::numeric_limits::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(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 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(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 generator = + registry.CreateCompatibleGenerator(data); + ASSERT_TRUE(generator); + + auto descriptor = fml::MakeRefCounted(std::move(data), + std::move(generator)); + + ASSERT_FALSE( + IsPngWithPLTE(descriptor->data()->bytes(), descriptor->data()->size())); + +#if IMPELLER_SUPPORTS_RENDERING + std::shared_ptr allocator = + std::make_shared(); + std::optional 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(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 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 generator = + registry.CreateCompatibleGenerator(data); + ASSERT_TRUE(generator); + + auto descriptor = fml::MakeRefCounted(std::move(data), + std::move(generator)); + + ASSERT_TRUE( + IsPngWithPLTE(descriptor->data()->bytes(), descriptor->data()->size())); + +#if IMPELLER_SUPPORTS_RENDERING + std::shared_ptr allocator = + std::make_shared(); + std::optional 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(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 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 diff --git a/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.h b/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.h new file mode 100644 index 00000000000..eef8a6dd3c5 --- /dev/null +++ b/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.h @@ -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 + +#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 mapping, + size_t slice) { + return true; + } +}; + +class TestImpellerDeviceBuffer : public DeviceBuffer { + public: + explicit TestImpellerDeviceBuffer(DeviceBufferDescriptor desc) + : DeviceBuffer(desc) { + bytes_ = static_cast(malloc(desc.size)); + } + + ~TestImpellerDeviceBuffer() { free(bytes_); } + + private: + std::shared_ptr 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 OnCreateBuffer( + const DeviceBufferDescriptor& desc) override { + return std::make_shared(desc); + } + + std::shared_ptr OnCreateTexture( + const TextureDescriptor& desc) override { + return std::make_shared(desc); + } +}; + +} // namespace impeller + +namespace flutter { +namespace testing { + +float HalfToFloat(uint16_t half); +float DecodeBGR10(uint32_t x); +sk_sp OpenFixtureAsSkData(const char* name); + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/lib/ui/painting/image_decoder_unittests.cc b/engine/src/flutter/lib/ui/painting/image_decoder_unittests.cc index b779a77ded1..9c2962f843c 100644 --- a/engine/src/flutter/lib/ui/painting/image_decoder_unittests.cc +++ b/engine/src/flutter/lib/ui/painting/image_decoder_unittests.cc @@ -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 mapping, - size_t slice) { - return true; - } -}; - -class TestImpellerDeviceBuffer : public DeviceBuffer { - public: - explicit TestImpellerDeviceBuffer(DeviceBufferDescriptor desc) - : DeviceBuffer(desc) { - bytes_ = static_cast(malloc(desc.size)); - } - - ~TestImpellerDeviceBuffer() { free(bytes_); } - - private: - std::shared_ptr 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 OnCreateBuffer( - const DeviceBufferDescriptor& desc) override { - return std::make_shared(desc); - } - - std::shared_ptr OnCreateTexture( - const TextureDescriptor& desc) override { - return std::make_shared(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 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(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::infinity(); - case 0xfc00: - return -std::numeric_limits::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(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 generator = - registry.CreateCompatibleGenerator(data); - ASSERT_TRUE(generator); - - auto descriptor = fml::MakeRefCounted(std::move(data), - std::move(generator)); - -#if IMPELLER_SUPPORTS_RENDERING - std::shared_ptr allocator = - std::make_shared(); - std::optional 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(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 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);