Skip to content

Commit

Permalink
[Impeller] Made toByteData work on wide gamut images (always downscal…
Browse files Browse the repository at this point in the history
…ing to sRGB). (flutter#39932)

[Impeller] Made toByteData work on wide gamut images (always downscaling to sRGB).
  • Loading branch information
gaaclarke authored Mar 2, 2023
1 parent 0c9323e commit 0562901
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 30 deletions.
1 change: 1 addition & 0 deletions ci/licenses_golden/excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
../../../flutter/impeller/renderer/pipeline_descriptor_unittests.cc
../../../flutter/impeller/renderer/renderer_dart_unittests.cc
../../../flutter/impeller/renderer/renderer_unittests.cc
../../../flutter/impeller/renderer/testing
../../../flutter/impeller/runtime_stage/runtime_stage_unittests.cc
../../../flutter/impeller/scene/README.md
../../../flutter/impeller/scene/importer/importer_unittests.cc
Expand Down
122 changes: 122 additions & 0 deletions impeller/renderer/testing/mocks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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.

#pragma once

#include "gmock/gmock.h"
#include "impeller/renderer/allocator.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/renderer/context.h"
#include "impeller/renderer/render_target.h"
#include "impeller/renderer/texture.h"

namespace impeller {
namespace testing {

class MockDeviceBuffer : public DeviceBuffer {
public:
MockDeviceBuffer(const DeviceBufferDescriptor& desc) : DeviceBuffer(desc) {}
MOCK_METHOD3(CopyHostBuffer,
bool(const uint8_t* source, Range source_range, size_t offset));

MOCK_METHOD1(SetLabel, bool(const std::string& label));

MOCK_METHOD2(SetLabel, bool(const std::string& label, Range range));

MOCK_CONST_METHOD0(OnGetContents, uint8_t*());

MOCK_METHOD3(OnCopyHostBuffer,
bool(const uint8_t* source, Range source_range, size_t offset));
};

class MockAllocator : public Allocator {
public:
MOCK_CONST_METHOD0(GetMaxTextureSizeSupported, ISize());
MOCK_METHOD1(
OnCreateBuffer,
std::shared_ptr<DeviceBuffer>(const DeviceBufferDescriptor& desc));
MOCK_METHOD1(OnCreateTexture,
std::shared_ptr<Texture>(const TextureDescriptor& desc));
};

class MockBlitPass : public BlitPass {
public:
MOCK_CONST_METHOD0(IsValid, bool());
MOCK_CONST_METHOD1(
EncodeCommands,
bool(const std::shared_ptr<Allocator>& transients_allocator));
MOCK_METHOD1(OnSetLabel, void(std::string label));

MOCK_METHOD5(OnCopyTextureToTextureCommand,
bool(std::shared_ptr<Texture> source,
std::shared_ptr<Texture> destination,
IRect source_region,
IPoint destination_origin,
std::string label));

MOCK_METHOD5(OnCopyTextureToBufferCommand,
bool(std::shared_ptr<Texture> source,
std::shared_ptr<DeviceBuffer> destination,
IRect source_region,
size_t destination_offset,
std::string label));

MOCK_METHOD2(OnGenerateMipmapCommand,
bool(std::shared_ptr<Texture> texture, std::string label));
};

class MockCommandBuffer : public CommandBuffer {
public:
MockCommandBuffer(std::weak_ptr<const Context> context)
: CommandBuffer(context) {}
MOCK_CONST_METHOD0(IsValid, bool());
MOCK_CONST_METHOD1(SetLabel, void(const std::string& label));
MOCK_CONST_METHOD0(OnCreateBlitPass, std::shared_ptr<BlitPass>());
MOCK_METHOD1(OnSubmitCommands, bool(CompletionCallback callback));
MOCK_CONST_METHOD0(OnCreateComputePass, std::shared_ptr<ComputePass>());
MOCK_METHOD1(OnCreateRenderPass,
std::shared_ptr<RenderPass>(RenderTarget render_target));
};

class MockImpellerContext : public Context {
public:
MOCK_CONST_METHOD0(IsValid, bool());

MOCK_CONST_METHOD0(GetResourceAllocator, std::shared_ptr<Allocator>());

MOCK_CONST_METHOD0(GetShaderLibrary, std::shared_ptr<ShaderLibrary>());

MOCK_CONST_METHOD0(GetSamplerLibrary, std::shared_ptr<SamplerLibrary>());

MOCK_CONST_METHOD0(GetPipelineLibrary, std::shared_ptr<PipelineLibrary>());

MOCK_CONST_METHOD0(CreateCommandBuffer, std::shared_ptr<CommandBuffer>());

MOCK_CONST_METHOD0(GetWorkQueue, std::shared_ptr<WorkQueue>());

MOCK_CONST_METHOD0(GetGPUTracer, std::shared_ptr<GPUTracer>());

MOCK_CONST_METHOD0(GetColorAttachmentPixelFormat, PixelFormat());

MOCK_CONST_METHOD0(GetDeviceCapabilities, const IDeviceCapabilities&());
};

class MockTexture : public Texture {
public:
MockTexture(const TextureDescriptor& desc) : Texture(desc) {}
MOCK_METHOD1(SetLabel, void(std::string_view label));
MOCK_METHOD3(SetContents,
bool(const uint8_t* contents, size_t length, size_t slice));
MOCK_METHOD2(SetContents,
bool(std::shared_ptr<const fml::Mapping> mapping, size_t slice));
MOCK_CONST_METHOD0(IsValid, bool());
MOCK_CONST_METHOD0(GetSize, ISize());
MOCK_METHOD3(OnSetContents,
bool(const uint8_t* contents, size_t length, size_t slice));
MOCK_METHOD2(OnSetContents,
bool(std::shared_ptr<const fml::Mapping> mapping, size_t slice));
};

} // namespace testing
} // namespace impeller
6 changes: 3 additions & 3 deletions lib/ui/painting/image_encoding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ void EncodeImageAndInvokeDataCallback(
FML_DCHECK(image);
#if IMPELLER_SUPPORTS_RENDERING
if (is_impeller_enabled) {
ConvertImageToRasterImpeller(image, encode_task, raster_task_runner,
io_task_runner, is_gpu_disabled_sync_switch,
impeller_context);
ImageEncodingImpeller::ConvertImageToRaster(
image, encode_task, raster_task_runner, io_task_runner,
is_gpu_disabled_sync_switch, impeller_context);
return;
}
#endif // IMPELLER_SUPPORTS_RENDERING
Expand Down
44 changes: 24 additions & 20 deletions lib/ui/painting/image_encoding_impeller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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_encoding_impeller.h"

#include "flutter/lib/ui/painting/image.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/renderer/context.h"
Expand All @@ -15,12 +17,14 @@ std::optional<SkColorType> ToSkColorType(impeller::PixelFormat format) {
switch (format) {
case impeller::PixelFormat::kR8G8B8A8UNormInt:
return SkColorType::kRGBA_8888_SkColorType;
case impeller::PixelFormat::kR16G16B16A16Float:
return SkColorType::kRGBA_F16_SkColorType;
case impeller::PixelFormat::kB8G8R8A8UNormInt:
return SkColorType::kBGRA_8888_SkColorType;
break;
case impeller::PixelFormat::kB10G10R10XR:
return SkColorType::kBGR_101010x_XR_SkColorType;
default:
return std::nullopt;
break;
}
}

Expand Down Expand Up @@ -50,7 +54,23 @@ sk_sp<SkImage> ConvertBufferToSkImage(
return raster_image;
}

void ConvertDlImageImpellerToSkImage(
void DoConvertImageToRasterImpeller(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const std::shared_ptr<const fml::SyncSwitch>& is_gpu_disabled_sync_switch,
const std::shared_ptr<impeller::Context>& impeller_context) {
is_gpu_disabled_sync_switch->Execute(
fml::SyncSwitch::Handlers()
.SetIfTrue([&encode_task] { encode_task(nullptr); })
.SetIfFalse([&dl_image, &encode_task, &impeller_context] {
ImageEncodingImpeller::ConvertDlImageToSkImage(
dl_image, std::move(encode_task), impeller_context);
}));
}

} // namespace

void ImageEncodingImpeller::ConvertDlImageToSkImage(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const std::shared_ptr<impeller::Context>& impeller_context) {
Expand Down Expand Up @@ -111,23 +131,7 @@ void ConvertDlImageImpellerToSkImage(
}
}

void DoConvertImageToRasterImpeller(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const std::shared_ptr<const fml::SyncSwitch>& is_gpu_disabled_sync_switch,
const std::shared_ptr<impeller::Context>& impeller_context) {
is_gpu_disabled_sync_switch->Execute(
fml::SyncSwitch::Handlers()
.SetIfTrue([&encode_task] { encode_task(nullptr); })
.SetIfFalse([&dl_image, &encode_task, &impeller_context] {
ConvertDlImageImpellerToSkImage(dl_image, std::move(encode_task),
impeller_context);
}));
}

} // namespace

void ConvertImageToRasterImpeller(
void ImageEncodingImpeller::ConvertImageToRaster(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const fml::RefPtr<fml::TaskRunner>& raster_task_runner,
Expand Down
28 changes: 21 additions & 7 deletions lib/ui/painting/image_encoding_impeller.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,28 @@ class Context;

namespace flutter {

void ConvertImageToRasterImpeller(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const fml::RefPtr<fml::TaskRunner>& raster_task_runner,
const fml::RefPtr<fml::TaskRunner>& io_task_runner,
const std::shared_ptr<const fml::SyncSwitch>& is_gpu_disabled_sync_switch,
const std::shared_ptr<impeller::Context>& impeller_context);
class ImageEncodingImpeller {
public:
/// Converts a DlImage to a SkImage.
/// This should be called from the thread that corresponds to
/// `dl_image->owning_context()` when gpu access is guaranteed.
/// See also: `ConvertImageToRaster`.
/// Visible for testing.
static void ConvertDlImageToSkImage(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const std::shared_ptr<impeller::Context>& impeller_context);

/// Converts a DlImage to a SkImage.
/// `encode_task` is executed with the resulting `SkImage`.
static void ConvertImageToRaster(
const sk_sp<DlImage>& dl_image,
std::function<void(sk_sp<SkImage>)> encode_task,
const fml::RefPtr<fml::TaskRunner>& raster_task_runner,
const fml::RefPtr<fml::TaskRunner>& io_task_runner,
const std::shared_ptr<const fml::SyncSwitch>& is_gpu_disabled_sync_switch,
const std::shared_ptr<impeller::Context>& impeller_context);
};
} // namespace flutter

#endif // FLUTTER_LIB_UI_PAINTING_IMAGE_ENCODING_IMPELLER_H_
107 changes: 107 additions & 0 deletions lib/ui/painting/image_encoding_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
#include "flutter/shell/common/thread_host.h"
#include "flutter/testing/testing.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

#if IMPELLER_SUPPORTS_RENDERING
#include "flutter/lib/ui/painting/image_encoding_impeller.h"
#include "impeller/renderer/testing/mocks.h"
#endif // IMPELLER_SUPPORTS_RENDERING

// CREATE_NATIVE_ENTRY is leaky by design
// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
Expand All @@ -22,8 +28,19 @@ namespace testing {

namespace {
fml::AutoResetWaitableEvent message_latch;

class MockDlImage : public DlImage {
public:
MOCK_CONST_METHOD0(skia_image, sk_sp<SkImage>());
MOCK_CONST_METHOD0(impeller_texture, std::shared_ptr<impeller::Texture>());
MOCK_CONST_METHOD0(isOpaque, bool());
MOCK_CONST_METHOD0(isTextureBacked, bool());
MOCK_CONST_METHOD0(dimensions, SkISize());
MOCK_CONST_METHOD0(GetApproximateByteSize, size_t());
};

} // namespace

class MockSyncSwitch {
public:
struct Handlers {
Expand Down Expand Up @@ -166,6 +183,96 @@ TEST_F(ShellTest, EncodeImageAccessesSyncSwitch) {
DestroyShell(std::move(shell), task_runners);
}

#if IMPELLER_SUPPORTS_RENDERING
using ::impeller::testing::MockAllocator;
using ::impeller::testing::MockBlitPass;
using ::impeller::testing::MockCommandBuffer;
using ::impeller::testing::MockDeviceBuffer;
using ::impeller::testing::MockImpellerContext;
using ::impeller::testing::MockTexture;
using ::testing::_;
using ::testing::DoAll;
using ::testing::InvokeArgument;
using ::testing::Return;

namespace {
std::shared_ptr<impeller::Context> MakeConvertDlImageToSkImageContext(
std::vector<uint8_t>& buffer) {
auto context = std::make_shared<MockImpellerContext>();
auto command_buffer = std::make_shared<MockCommandBuffer>(context);
auto allocator = std::make_shared<MockAllocator>();
auto blit_pass = std::make_shared<MockBlitPass>();
impeller::DeviceBufferDescriptor device_buffer_desc;
device_buffer_desc.size = buffer.size();
auto device_buffer = std::make_shared<MockDeviceBuffer>(device_buffer_desc);
EXPECT_CALL(*allocator, OnCreateBuffer).WillOnce(Return(device_buffer));
EXPECT_CALL(*blit_pass, IsValid).WillRepeatedly(Return(true));
EXPECT_CALL(*command_buffer, IsValid).WillRepeatedly(Return(true));
EXPECT_CALL(*command_buffer, OnCreateBlitPass).WillOnce(Return(blit_pass));
EXPECT_CALL(*command_buffer, OnSubmitCommands(_))
.WillOnce(
DoAll(InvokeArgument<0>(impeller::CommandBuffer::Status::kCompleted),
Return(true)));
EXPECT_CALL(*context, GetResourceAllocator).WillRepeatedly(Return(allocator));
EXPECT_CALL(*context, CreateCommandBuffer).WillOnce(Return(command_buffer));
EXPECT_CALL(*device_buffer, OnGetContents).WillOnce(Return(buffer.data()));
return context;
}
} // namespace

TEST(ImageEncodingImpellerTest, ConvertDlImageToSkImage16Float) {
sk_sp<MockDlImage> image(new MockDlImage());
EXPECT_CALL(*image, dimensions)
.WillRepeatedly(Return(SkISize::Make(100, 100)));
impeller::TextureDescriptor desc;
desc.format = impeller::PixelFormat::kR16G16B16A16Float;
auto texture = std::make_shared<MockTexture>(desc);
EXPECT_CALL(*image, impeller_texture).WillOnce(Return(texture));
std::vector<uint8_t> buffer;
buffer.reserve(100 * 100 * 8);
auto context = MakeConvertDlImageToSkImageContext(buffer);
bool did_call = false;
ImageEncodingImpeller::ConvertDlImageToSkImage(
image,
[&did_call](const sk_sp<SkImage>& image) {
did_call = true;
ASSERT_TRUE(image);
EXPECT_EQ(100, image->width());
EXPECT_EQ(100, image->height());
EXPECT_EQ(kRGBA_F16_SkColorType, image->colorType());
EXPECT_EQ(nullptr, image->colorSpace());
},
context);
EXPECT_TRUE(did_call);
}

TEST(ImageEncodingImpellerTest, ConvertDlImageToSkImage10XR) {
sk_sp<MockDlImage> image(new MockDlImage());
EXPECT_CALL(*image, dimensions)
.WillRepeatedly(Return(SkISize::Make(100, 100)));
impeller::TextureDescriptor desc;
desc.format = impeller::PixelFormat::kB10G10R10XR;
auto texture = std::make_shared<MockTexture>(desc);
EXPECT_CALL(*image, impeller_texture).WillOnce(Return(texture));
std::vector<uint8_t> buffer;
buffer.reserve(100 * 100 * 4);
auto context = MakeConvertDlImageToSkImageContext(buffer);
bool did_call = false;
ImageEncodingImpeller::ConvertDlImageToSkImage(
image,
[&did_call](const sk_sp<SkImage>& image) {
did_call = true;
ASSERT_TRUE(image);
EXPECT_EQ(100, image->width());
EXPECT_EQ(100, image->height());
EXPECT_EQ(kBGR_101010x_XR_SkColorType, image->colorType());
EXPECT_EQ(nullptr, image->colorSpace());
},
context);
EXPECT_TRUE(did_call);
}
#endif // IMPELLER_SUPPORTS_RENDERING

} // namespace testing
} // namespace flutter

Expand Down

0 comments on commit 0562901

Please sign in to comment.