blob: d90378dd625fe935e7d107d63bb6368b5fc5e4f1 [file] [log] [blame]
// 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/common/task_runners.h"
#include "flutter/fml/mapping.h"
#include "flutter/fml/synchronization/waitable_event.h"
#include "flutter/lib/ui/painting/image_decoder.h"
#include "flutter/lib/ui/painting/image_decoder_test.h"
#include "flutter/lib/ui/painting/multi_frame_codec.h"
#include "flutter/runtime/dart_vm.h"
#include "flutter/runtime/dart_vm_lifecycle.h"
#include "flutter/testing/dart_isolate_runner.h"
#include "flutter/testing/elf_loader.h"
#include "flutter/testing/test_dart_native_resolver.h"
#include "flutter/testing/test_gl_surface.h"
#include "flutter/testing/testing.h"
#include "flutter/testing/thread_test.h"
#include "third_party/skia/include/codec/SkCodec.h"
namespace flutter {
namespace testing {
class TestIOManager final : public IOManager {
public:
TestIOManager(fml::RefPtr<fml::TaskRunner> task_runner,
bool has_gpu_context = true)
: gl_surface_(SkISize::Make(1, 1)),
gl_context_(has_gpu_context ? gl_surface_.CreateGrContext() : nullptr),
weak_gl_context_factory_(
has_gpu_context ? std::make_unique<fml::WeakPtrFactory<GrContext>>(
gl_context_.get())
: nullptr),
unref_queue_(fml::MakeRefCounted<SkiaUnrefQueue>(
task_runner,
fml::TimeDelta::FromNanoseconds(0))),
runner_(task_runner),
weak_factory_(this),
is_gpu_disabled_sync_switch_(std::make_shared<fml::SyncSwitch>()) {
FML_CHECK(task_runner->RunsTasksOnCurrentThread())
<< "The IO manager must be initialized its primary task runner. The "
"test harness may not be setup correctly/safely.";
weak_prototype_ = weak_factory_.GetWeakPtr();
}
~TestIOManager() override {
fml::AutoResetWaitableEvent latch;
fml::TaskRunner::RunNowOrPostTask(runner_,
[&latch, queue = unref_queue_]() {
queue->Drain();
latch.Signal();
});
latch.Wait();
}
// |IOManager|
fml::WeakPtr<IOManager> GetWeakIOManager() const override {
return weak_prototype_;
}
// |IOManager|
fml::WeakPtr<GrContext> GetResourceContext() const override {
return weak_gl_context_factory_ ? weak_gl_context_factory_->GetWeakPtr()
: fml::WeakPtr<GrContext>{};
}
// |IOManager|
fml::RefPtr<flutter::SkiaUnrefQueue> GetSkiaUnrefQueue() const override {
return unref_queue_;
}
// |IOManager|
std::shared_ptr<fml::SyncSwitch> GetIsGpuDisabledSyncSwitch() override {
did_access_is_gpu_disabled_sync_switch_ = true;
return is_gpu_disabled_sync_switch_;
}
bool did_access_is_gpu_disabled_sync_switch_ = false;
private:
TestGLSurface gl_surface_;
sk_sp<GrContext> gl_context_;
std::unique_ptr<fml::WeakPtrFactory<GrContext>> weak_gl_context_factory_;
fml::RefPtr<SkiaUnrefQueue> unref_queue_;
fml::WeakPtr<TestIOManager> weak_prototype_;
fml::RefPtr<fml::TaskRunner> runner_;
fml::WeakPtrFactory<TestIOManager> weak_factory_;
std::shared_ptr<fml::SyncSwitch> is_gpu_disabled_sync_switch_;
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;
}
TEST_F(ImageDecoderFixtureTest, CanCreateImageDecoder) {
auto loop = fml::ConcurrentMessageLoop::Create();
auto thread_task_runner = CreateNewThread();
TaskRunners runners(GetCurrentTestName(), // label
thread_task_runner, // platform
thread_task_runner, // raster
thread_task_runner, // ui
thread_task_runner // io
);
fml::AutoResetWaitableEvent latch;
runners.GetIOTaskRunner()->PostTask([&]() {
TestIOManager manager(runners.GetIOTaskRunner());
ImageDecoder decoder(std::move(runners), loop->GetTaskRunner(),
manager.GetWeakIOManager());
latch.Signal();
});
latch.Wait();
}
TEST_F(ImageDecoderFixtureTest, InvalidImageResultsError) {
auto loop = fml::ConcurrentMessageLoop::Create();
auto thread_task_runner = CreateNewThread();
TaskRunners runners(GetCurrentTestName(), // label
thread_task_runner, // platform
thread_task_runner, // raster
thread_task_runner, // ui
thread_task_runner // io
);
fml::AutoResetWaitableEvent latch;
thread_task_runner->PostTask([&]() {
TestIOManager manager(runners.GetIOTaskRunner());
ImageDecoder decoder(runners, loop->GetTaskRunner(),
manager.GetWeakIOManager());
ImageDecoder::ImageDescriptor image_descriptor;
image_descriptor.data = OpenFixtureAsSkData("ThisDoesNotExist.jpg");
ASSERT_FALSE(image_descriptor.data);
ImageDecoder::ImageResult callback = [&](SkiaGPUObject<SkImage> image) {
ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
ASSERT_FALSE(image.get());
latch.Signal();
};
decoder.Decode(std::move(image_descriptor), callback);
});
latch.Wait();
}
TEST_F(ImageDecoderFixtureTest, ValidImageResultsInSuccess) {
auto loop = fml::ConcurrentMessageLoop::Create();
TaskRunners runners(GetCurrentTestName(), // label
CreateNewThread("platform"), // platform
CreateNewThread("raster"), // raster
CreateNewThread("ui"), // ui
CreateNewThread("io") // io
);
fml::AutoResetWaitableEvent latch;
std::unique_ptr<TestIOManager> io_manager;
auto release_io_manager = [&]() {
io_manager.reset();
latch.Signal();
};
auto decode_image = [&]() {
std::unique_ptr<ImageDecoder> image_decoder =
std::make_unique<ImageDecoder>(runners, loop->GetTaskRunner(),
io_manager->GetWeakIOManager());
ImageDecoder::ImageDescriptor image_descriptor;
image_descriptor.data = OpenFixtureAsSkData("DashInNooglerHat.jpg");
ASSERT_TRUE(image_descriptor.data);
ASSERT_GE(image_descriptor.data->size(), 0u);
ImageDecoder::ImageResult callback = [&](SkiaGPUObject<SkImage> image) {
ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
ASSERT_TRUE(image.get());
EXPECT_TRUE(io_manager->did_access_is_gpu_disabled_sync_switch_);
runners.GetIOTaskRunner()->PostTask(release_io_manager);
};
EXPECT_FALSE(io_manager->did_access_is_gpu_disabled_sync_switch_);
image_decoder->Decode(std::move(image_descriptor), callback);
};
auto setup_io_manager_and_decode = [&]() {
io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
runners.GetUITaskRunner()->PostTask(decode_image);
};
runners.GetIOTaskRunner()->PostTask(setup_io_manager_and_decode);
latch.Wait();
}
TEST_F(ImageDecoderFixtureTest, ExifDataIsRespectedOnDecode) {
auto loop = fml::ConcurrentMessageLoop::Create();
TaskRunners runners(GetCurrentTestName(), // label
CreateNewThread("platform"), // platform
CreateNewThread("raster"), // raster
CreateNewThread("ui"), // ui
CreateNewThread("io") // io
);
fml::AutoResetWaitableEvent latch;
std::unique_ptr<IOManager> io_manager;
auto release_io_manager = [&]() {
io_manager.reset();
latch.Signal();
};
SkISize decoded_size = SkISize::MakeEmpty();
auto decode_image = [&]() {
std::unique_ptr<ImageDecoder> image_decoder =
std::make_unique<ImageDecoder>(runners, loop->GetTaskRunner(),
io_manager->GetWeakIOManager());
ImageDecoder::ImageDescriptor image_descriptor;
image_descriptor.data = OpenFixtureAsSkData("Horizontal.jpg");
ASSERT_TRUE(image_descriptor.data);
ASSERT_GE(image_descriptor.data->size(), 0u);
ImageDecoder::ImageResult callback = [&](SkiaGPUObject<SkImage> image) {
ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
ASSERT_TRUE(image.get());
decoded_size = image.get()->dimensions();
runners.GetIOTaskRunner()->PostTask(release_io_manager);
};
image_decoder->Decode(std::move(image_descriptor), callback);
};
auto setup_io_manager_and_decode = [&]() {
io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
runners.GetUITaskRunner()->PostTask(decode_image);
};
runners.GetIOTaskRunner()->PostTask(setup_io_manager_and_decode);
latch.Wait();
ASSERT_EQ(decoded_size.width(), 600);
ASSERT_EQ(decoded_size.height(), 200);
}
TEST_F(ImageDecoderFixtureTest, CanDecodeWithoutAGPUContext) {
auto loop = fml::ConcurrentMessageLoop::Create();
TaskRunners runners(GetCurrentTestName(), // label
CreateNewThread("platform"), // platform
CreateNewThread("raster"), // raster
CreateNewThread("ui"), // ui
CreateNewThread("io") // io
);
fml::AutoResetWaitableEvent latch;
std::unique_ptr<IOManager> io_manager;
auto release_io_manager = [&]() {
io_manager.reset();
latch.Signal();
};
auto decode_image = [&]() {
std::unique_ptr<ImageDecoder> image_decoder =
std::make_unique<ImageDecoder>(runners, loop->GetTaskRunner(),
io_manager->GetWeakIOManager());
ImageDecoder::ImageDescriptor image_descriptor;
image_descriptor.data = OpenFixtureAsSkData("DashInNooglerHat.jpg");
ASSERT_TRUE(image_descriptor.data);
ASSERT_GE(image_descriptor.data->size(), 0u);
ImageDecoder::ImageResult callback = [&](SkiaGPUObject<SkImage> image) {
ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
ASSERT_TRUE(image.get());
runners.GetIOTaskRunner()->PostTask(release_io_manager);
};
image_decoder->Decode(std::move(image_descriptor), callback);
};
auto setup_io_manager_and_decode = [&]() {
io_manager =
std::make_unique<TestIOManager>(runners.GetIOTaskRunner(), false);
runners.GetUITaskRunner()->PostTask(decode_image);
};
runners.GetIOTaskRunner()->PostTask(setup_io_manager_and_decode);
latch.Wait();
}
TEST_F(ImageDecoderFixtureTest, CanDecodeWithResizes) {
const auto image_dimensions =
SkImage::MakeFromEncoded(OpenFixtureAsSkData("DashInNooglerHat.jpg"))
->dimensions();
ASSERT_FALSE(image_dimensions.isEmpty());
ASSERT_NE(image_dimensions.width(), image_dimensions.height());
auto loop = fml::ConcurrentMessageLoop::Create();
TaskRunners runners(GetCurrentTestName(), // label
CreateNewThread("platform"), // platform
CreateNewThread("raster"), // raster
CreateNewThread("ui"), // ui
CreateNewThread("io") // io
);
fml::AutoResetWaitableEvent latch;
std::unique_ptr<IOManager> io_manager;
std::unique_ptr<ImageDecoder> image_decoder;
// Setup the IO manager.
runners.GetIOTaskRunner()->PostTask([&]() {
io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
latch.Signal();
});
latch.Wait();
// Setup the image decoder.
runners.GetUITaskRunner()->PostTask([&]() {
image_decoder = std::make_unique<ImageDecoder>(
runners, loop->GetTaskRunner(), io_manager->GetWeakIOManager());
latch.Signal();
});
latch.Wait();
// Setup a generic decoding utility that gives us the final decoded size.
auto decoded_size = [&](std::optional<uint32_t> target_width,
std::optional<uint32_t> target_height) -> SkISize {
SkISize final_size = SkISize::MakeEmpty();
runners.GetUITaskRunner()->PostTask([&]() {
ImageDecoder::ImageDescriptor image_descriptor;
image_descriptor.target_width = target_width;
image_descriptor.target_height = target_height;
image_descriptor.data = OpenFixtureAsSkData("DashInNooglerHat.jpg");
ASSERT_TRUE(image_descriptor.data);
ASSERT_GE(image_descriptor.data->size(), 0u);
ImageDecoder::ImageResult callback = [&](SkiaGPUObject<SkImage> image) {
ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
ASSERT_TRUE(image.get());
final_size = image.get()->dimensions();
latch.Signal();
};
image_decoder->Decode(std::move(image_descriptor), callback);
});
latch.Wait();
return final_size;
};
ASSERT_EQ(SkISize::Make(3024, 4032), image_dimensions);
ASSERT_EQ(decoded_size({}, {}), image_dimensions);
ASSERT_EQ(decoded_size(100, {}), SkISize::Make(100, 133));
ASSERT_EQ(decoded_size({}, 100), SkISize::Make(75, 100));
ASSERT_EQ(decoded_size(100, 100), SkISize::Make(100, 100));
// Destroy the IO manager
runners.GetIOTaskRunner()->PostTask([&]() {
io_manager.reset();
latch.Signal();
});
latch.Wait();
// Destroy the image decoder
runners.GetUITaskRunner()->PostTask([&]() {
image_decoder.reset();
latch.Signal();
});
latch.Wait();
}
TEST_F(ImageDecoderFixtureTest, CanResizeWithoutDecode) {
ImageDecoder::ImageInfo info = {};
sk_sp<SkData> decompressed_data;
SkISize image_dimensions = SkISize::MakeEmpty();
{
auto image =
SkImage::MakeFromEncoded(OpenFixtureAsSkData("DashInNooglerHat.jpg"))
->makeRasterImage();
image_dimensions = image->dimensions();
SkPixmap pixmap;
ASSERT_TRUE(image->peekPixels(&pixmap));
info.sk_info = SkImageInfo::MakeN32Premul(image_dimensions);
info.row_bytes = pixmap.rowBytes();
decompressed_data =
SkData::MakeWithCopy(pixmap.writable_addr(), pixmap.computeByteSize());
}
// This is not susecptible to changes in the underlying image decoder.
ASSERT_EQ(decompressed_data->size(), 48771072u);
ASSERT_EQ(decompressed_data->size(),
image_dimensions.width() * image_dimensions.height() * 4u);
ASSERT_EQ(info.row_bytes, image_dimensions.width() * 4u);
ASSERT_FALSE(image_dimensions.isEmpty());
ASSERT_NE(image_dimensions.width(), image_dimensions.height());
auto loop = fml::ConcurrentMessageLoop::Create();
TaskRunners runners(GetCurrentTestName(), // label
CreateNewThread("platform"), // platform
CreateNewThread("raster"), // raster
CreateNewThread("ui"), // ui
CreateNewThread("io") // io
);
fml::AutoResetWaitableEvent latch;
std::unique_ptr<IOManager> io_manager;
std::unique_ptr<ImageDecoder> image_decoder;
// Setup the IO manager.
runners.GetIOTaskRunner()->PostTask([&]() {
io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
latch.Signal();
});
latch.Wait();
// Setup the image decoder.
runners.GetUITaskRunner()->PostTask([&]() {
image_decoder = std::make_unique<ImageDecoder>(
runners, loop->GetTaskRunner(), io_manager->GetWeakIOManager());
latch.Signal();
});
latch.Wait();
// Setup a generic decoding utility that gives us the final decoded size.
auto decoded_size = [&](std::optional<uint32_t> target_width,
std::optional<uint32_t> target_height) -> SkISize {
SkISize final_size = SkISize::MakeEmpty();
runners.GetUITaskRunner()->PostTask([&]() {
ImageDecoder::ImageDescriptor image_descriptor;
image_descriptor.target_width = target_width;
image_descriptor.target_height = target_height;
image_descriptor.data = decompressed_data;
image_descriptor.decompressed_image_info = info;
ASSERT_TRUE(image_descriptor.data);
ASSERT_GE(image_descriptor.data->size(), 0u);
ImageDecoder::ImageResult callback = [&](SkiaGPUObject<SkImage> image) {
ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
ASSERT_TRUE(image.get());
final_size = image.get()->dimensions();
latch.Signal();
};
image_decoder->Decode(std::move(image_descriptor), callback);
});
latch.Wait();
return final_size;
};
ASSERT_EQ(SkISize::Make(3024, 4032), image_dimensions);
ASSERT_EQ(decoded_size({}, {}), image_dimensions);
ASSERT_EQ(decoded_size(100, {}), SkISize::Make(100, 133));
ASSERT_EQ(decoded_size({}, 100), SkISize::Make(75, 100));
ASSERT_EQ(decoded_size(100, 100), SkISize::Make(100, 100));
// Destroy the IO manager
runners.GetIOTaskRunner()->PostTask([&]() {
io_manager.reset();
latch.Signal();
});
latch.Wait();
// Destroy the image decoder
runners.GetUITaskRunner()->PostTask([&]() {
image_decoder.reset();
latch.Signal();
});
latch.Wait();
}
// Verifies https://skia-review.googlesource.com/c/skia/+/259161 is present in
// Flutter.
TEST(ImageDecoderTest,
VerifyCodecRepeatCountsForGifAndWebPAreConsistentWithLoopCounts) {
auto gif_mapping = OpenFixtureAsSkData("hello_loop_2.gif");
auto webp_mapping = OpenFixtureAsSkData("hello_loop_2.webp");
ASSERT_TRUE(gif_mapping);
ASSERT_TRUE(webp_mapping);
auto gif_codec = SkCodec::MakeFromData(gif_mapping);
auto webp_codec = SkCodec::MakeFromData(webp_mapping);
ASSERT_TRUE(gif_codec);
ASSERT_TRUE(webp_codec);
// Both fixtures have a loop count of 2 which should lead to the repeat count
// of 1
ASSERT_EQ(gif_codec->getRepetitionCount(), 1);
ASSERT_EQ(webp_codec->getRepetitionCount(), 1);
}
TEST(ImageDecoderTest, VerifySimpleDecoding) {
auto data = OpenFixtureAsSkData("Horizontal.jpg");
auto image = SkImage::MakeFromEncoded(data);
ASSERT_TRUE(image != nullptr);
ASSERT_EQ(SkISize::Make(600, 200), image->dimensions());
ASSERT_EQ(ImageFromCompressedData(data, 6, 2, fml::tracing::TraceFlow(""))
->dimensions(),
SkISize::Make(6, 2));
}
TEST(ImageDecoderTest, VerifySubpixelDecodingPreservesExifOrientation) {
auto data = OpenFixtureAsSkData("Horizontal.jpg");
auto image = SkImage::MakeFromEncoded(data);
ASSERT_TRUE(image != nullptr);
ASSERT_EQ(SkISize::Make(600, 200), image->dimensions());
auto decode = [data](std::optional<uint32_t> target_width,
std::optional<uint32_t> target_height) {
return ImageFromCompressedData(data, target_width, target_height,
fml::tracing::TraceFlow(""));
};
auto expected_data = OpenFixtureAsSkData("Horizontal.png");
ASSERT_TRUE(expected_data != nullptr);
ASSERT_FALSE(expected_data->isEmpty());
auto assert_image = [&](auto decoded_image) {
ASSERT_EQ(decoded_image->dimensions(), SkISize::Make(300, 100));
ASSERT_TRUE(decoded_image->encodeToData(SkEncodedImageFormat::kPNG, 100)
->equals(expected_data.get()));
};
assert_image(decode(300, 100));
assert_image(decode(300, {}));
assert_image(decode({}, 100));
}
TEST_F(ImageDecoderFixtureTest,
MultiFrameCodecCanBeCollectedBeforeIOTasksFinish) {
// This test verifies that the MultiFrameCodec safely shares state between
// tasks on the IO and UI runners, and does not allow unsafe memory access if
// the UI object is collected while the IO thread still has pending decode
// work. This could happen in a real application if the engine is collected
// while a multi-frame image is decoding. To exercise this, the test:
// - Starts a Dart VM
// - Latches the IO task runner
// - Create a MultiFrameCodec for an animated gif pointed to a callback
// in the Dart fixture
// - Calls getNextFrame on the UI task runner
// - Collects the MultiFrameCodec object before unlatching the IO task
// runner.
// - Unlatches the IO task runner
auto settings = CreateSettingsForFixture();
auto vm_ref = DartVMRef::Create(settings);
auto vm_data = vm_ref.GetVMData();
auto gif_mapping = OpenFixtureAsSkData("hello_loop_2.gif");
ASSERT_TRUE(gif_mapping);
auto gif_codec = SkCodec::MakeFromData(gif_mapping);
ASSERT_TRUE(gif_codec);
TaskRunners runners(GetCurrentTestName(), // label
CreateNewThread("platform"), // platform
CreateNewThread("raster"), // raster
CreateNewThread("ui"), // ui
CreateNewThread("io") // io
);
fml::AutoResetWaitableEvent latch;
fml::AutoResetWaitableEvent io_latch;
std::unique_ptr<TestIOManager> io_manager;
// Setup the IO manager.
runners.GetIOTaskRunner()->PostTask([&]() {
io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
latch.Signal();
});
latch.Wait();
auto isolate =
RunDartCodeInIsolate(vm_ref, settings, runners, "main", {},
GetFixturesPath(), io_manager->GetWeakIOManager());
// Latch the IO task runner.
runners.GetIOTaskRunner()->PostTask([&]() { io_latch.Wait(); });
runners.GetUITaskRunner()->PostTask([&]() {
fml::AutoResetWaitableEvent isolate_latch;
fml::RefPtr<MultiFrameCodec> codec;
EXPECT_TRUE(isolate->RunInIsolateScope([&]() -> bool {
Dart_Handle library = Dart_RootLibrary();
if (Dart_IsError(library)) {
isolate_latch.Signal();
return false;
}
Dart_Handle closure =
Dart_GetField(library, Dart_NewStringFromCString("frameCallback"));
if (Dart_IsError(closure) || !Dart_IsClosure(closure)) {
isolate_latch.Signal();
return false;
}
codec = fml::MakeRefCounted<MultiFrameCodec>(std::move(gif_codec));
codec->getNextFrame(closure);
codec = nullptr;
isolate_latch.Signal();
return true;
}));
isolate_latch.Wait();
EXPECT_FALSE(codec);
io_latch.Signal();
latch.Signal();
});
latch.Wait();
// Destroy the IO manager
runners.GetIOTaskRunner()->PostTask([&]() {
io_manager.reset();
latch.Signal();
});
latch.Wait();
}
} // namespace testing
} // namespace flutter