Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
image_decoder_unittests.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
26#include "fml/logging.h"
29#include "third_party/skia/include/codec/SkCodec.h"
30#include "third_party/skia/include/codec/SkCodecAnimation.h"
31#include "third_party/skia/include/codec/SkJpegDecoder.h"
32#include "third_party/skia/include/core/SkData.h"
33#include "third_party/skia/include/core/SkImage.h"
34#include "third_party/skia/include/core/SkImageInfo.h"
35#include "third_party/skia/include/core/SkSize.h"
36#include "third_party/skia/include/encode/SkPngEncoder.h"
37
38// CREATE_NATIVE_ENTRY is leaky by design
39// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
40// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
41
42namespace impeller {
43
45 public:
47
48 BackendType GetBackendType() const override { return BackendType::kMetal; }
49
50 std::string DescribeGpuModel() const override { return "TestGpu"; }
51
52 bool IsValid() const override { return true; }
53
54 const std::shared_ptr<const Capabilities>& GetCapabilities() const override {
55 return capabilities_;
56 }
57
58 std::shared_ptr<Allocator> GetResourceAllocator() const override {
59 return std::make_shared<TestImpellerAllocator>();
60 }
61
62 std::shared_ptr<ShaderLibrary> GetShaderLibrary() const override {
63 return nullptr;
64 }
65
66 std::shared_ptr<SamplerLibrary> GetSamplerLibrary() const override {
67 return nullptr;
68 }
69
70 std::shared_ptr<PipelineLibrary> GetPipelineLibrary() const override {
71 return nullptr;
72 }
73
74 std::shared_ptr<CommandQueue> GetCommandQueue() const override {
76 }
77
78 std::shared_ptr<CommandBuffer> CreateCommandBuffer() const override {
80 return nullptr;
81 }
82
83 void StoreTaskForGPU(const std::function<void()>& task,
84 const std::function<void()>& failure) override {
85 tasks_.push_back(PendingTask{task, failure});
86 }
87
88 void FlushTasks(bool fail = false) {
89 for (auto& task : tasks_) {
90 if (fail) {
91 task.task();
92 } else {
93 task.failure();
94 }
95 }
96 tasks_.clear();
97 }
98
99 void DisposeThreadLocalCachedResources() override { did_dispose_ = true; }
100
101 void Shutdown() override {}
102
106
107 bool DidDisposeResources() const { return did_dispose_; }
108
109 mutable size_t command_buffer_count_ = 0;
110
111 private:
112 struct PendingTask {
113 std::function<void()> task;
114 std::function<void()> failure;
115 };
116 std::vector<PendingTask> tasks_;
117 std::shared_ptr<const Capabilities> capabilities_;
118 bool did_dispose_ = false;
119};
120
121} // namespace impeller
122
123namespace flutter {
124namespace testing {
125
126class TestIOManager final : public IOManager {
127 public:
128 explicit TestIOManager(const fml::RefPtr<fml::TaskRunner>& task_runner,
129 bool has_gpu_context = true)
130 : gl_surface_(DlISize(1, 1)),
131 impeller_context_(std::make_shared<impeller::TestImpellerContext>()),
132 gl_context_(has_gpu_context ? gl_surface_.CreateGrContext() : nullptr),
133 weak_gl_context_factory_(
134 has_gpu_context
135 ? std::make_unique<fml::WeakPtrFactory<GrDirectContext>>(
136 gl_context_.get())
137 : nullptr),
138 unref_queue_(fml::MakeRefCounted<SkiaUnrefQueue>(
139 task_runner,
140 fml::TimeDelta::FromNanoseconds(0),
141 gl_context_)),
142 runner_(task_runner),
143 is_gpu_disabled_sync_switch_(std::make_shared<fml::SyncSwitch>()),
144 weak_factory_(this) {
145 FML_CHECK(task_runner->RunsTasksOnCurrentThread())
146 << "The IO manager must be initialized its primary task runner. The "
147 "test harness may not be set up correctly/safely.";
148 weak_prototype_ = weak_factory_.GetWeakPtr();
149 }
150
151 ~TestIOManager() override {
154 [&latch, queue = unref_queue_]() {
155 queue->Drain();
156 latch.Signal();
157 });
158 latch.Wait();
159 }
160
161 // |IOManager|
163 return weak_prototype_;
164 }
165
166 // |IOManager|
168 return weak_gl_context_factory_ ? weak_gl_context_factory_->GetWeakPtr()
170 }
171
172 // |IOManager|
174 return unref_queue_;
175 }
176
177 // |IOManager|
178 std::shared_ptr<const fml::SyncSwitch> GetIsGpuDisabledSyncSwitch() override {
180 return is_gpu_disabled_sync_switch_;
181 }
182
183 // |IOManager|
184 std::shared_ptr<impeller::Context> GetImpellerContext() const override {
185 return impeller_context_;
186 }
187
188 void SetGpuDisabled(bool disabled) {
189 is_gpu_disabled_sync_switch_->SetSwitch(disabled);
190 }
191
193
194 private:
195 TestGLSurface gl_surface_;
196 std::shared_ptr<impeller::Context> impeller_context_;
197 sk_sp<GrDirectContext> gl_context_;
198 std::unique_ptr<fml::WeakPtrFactory<GrDirectContext>>
199 weak_gl_context_factory_;
200 fml::RefPtr<SkiaUnrefQueue> unref_queue_;
201 fml::WeakPtr<TestIOManager> weak_prototype_;
203 std::shared_ptr<fml::SyncSwitch> is_gpu_disabled_sync_switch_;
205
207};
208
210
211TEST_F(ImageDecoderFixtureTest, CanCreateImageDecoder) {
213 auto thread_task_runner = CreateNewThread();
214 TaskRunners runners(GetCurrentTestName(), // label
215 thread_task_runner, // platform
216 thread_task_runner, // raster
217 thread_task_runner, // ui
218 thread_task_runner // io
219
220 );
221
222 PostTaskSync(runners.GetIOTaskRunner(), [&]() {
223 TestIOManager manager(runners.GetIOTaskRunner());
224 Settings settings;
225 auto decoder = ImageDecoder::Make(settings, runners, loop->GetTaskRunner(),
226 manager.GetWeakIOManager(),
227 std::make_shared<fml::SyncSwitch>());
228 ASSERT_NE(decoder, nullptr);
229 });
230}
231
232/// An Image generator that pretends it can't recognize the data it was given.
234 public:
235 UnknownImageGenerator() : info_(SkImageInfo::MakeUnknown()) {};
237 const SkImageInfo& GetInfo() { return info_; }
238
239 unsigned int GetFrameCount() const { return 1; }
240
241 unsigned int GetPlayCount() const { return 1; }
242
243 const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) {
244 return {std::nullopt, 0, SkCodecAnimation::DisposalMethod::kKeep};
245 }
246
247 SkISize GetScaledDimensions(float scale) {
248 return SkISize::Make(info_.width(), info_.height());
249 }
250
251 bool GetPixels(const SkImageInfo& info,
252 void* pixels,
253 size_t row_bytes,
254 unsigned int frame_index,
255 std::optional<unsigned int> prior_frame) {
256 return false;
257 };
258
259 private:
260 SkImageInfo info_;
261};
262
263TEST_F(ImageDecoderFixtureTest, InvalidImageResultsError) {
265 auto thread_task_runner = CreateNewThread();
266 TaskRunners runners(GetCurrentTestName(), // label
267 thread_task_runner, // platform
268 thread_task_runner, // raster
269 thread_task_runner, // ui
270 thread_task_runner // io
271 );
272
274 thread_task_runner->PostTask([&]() {
276 Settings settings;
277 auto decoder = ImageDecoder::Make(settings, runners, loop->GetTaskRunner(),
278 manager.GetWeakIOManager(),
279 std::make_shared<fml::SyncSwitch>());
280
281 auto data = flutter::testing::OpenFixtureAsSkData("ThisDoesNotExist.jpg");
282 ASSERT_FALSE(data);
283
284 fml::RefPtr<ImageDescriptor> image_descriptor =
285 fml::MakeRefCounted<ImageDescriptor>(
286 std::move(data), std::make_unique<UnknownImageGenerator>());
287
288 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
289 const std::string& decode_error) {
290 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
291 ASSERT_FALSE(image);
292 latch.Signal();
293 };
294 decoder->Decode(image_descriptor, {.target_width = 0, .target_height = 0},
295 callback);
296 });
297 latch.Wait();
298}
299
300TEST_F(ImageDecoderFixtureTest, ValidImageResultsInSuccess) {
302 TaskRunners runners(GetCurrentTestName(), // label
303 CreateNewThread("platform"), // platform
304 CreateNewThread("raster"), // raster
305 CreateNewThread("ui"), // ui
306 CreateNewThread("io") // io
307 );
308
310
311 std::unique_ptr<TestIOManager> io_manager;
312
313 auto release_io_manager = [&]() {
314 io_manager.reset();
315 latch.Signal();
316 };
317 auto decode_image = [&]() {
318 Settings settings;
319 std::unique_ptr<ImageDecoder> image_decoder = ImageDecoder::Make(
320 settings, runners, loop->GetTaskRunner(),
321 io_manager->GetWeakIOManager(), std::make_shared<fml::SyncSwitch>());
322
323 auto data = flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg");
324
325 ASSERT_TRUE(data);
326 ASSERT_GE(data->size(), 0u);
327
328 ImageGeneratorRegistry registry;
329 std::shared_ptr<ImageGenerator> generator =
331 ASSERT_TRUE(generator);
332
333 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
334 std::move(data), std::move(generator));
335
336 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
337 const std::string& decode_error) {
338 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
339 ASSERT_TRUE(image && image->skia_image());
340 EXPECT_TRUE(io_manager->did_access_is_gpu_disabled_sync_switch_);
341 runners.GetIOTaskRunner()->PostTask(release_io_manager);
342 };
343 EXPECT_FALSE(io_manager->did_access_is_gpu_disabled_sync_switch_);
344 image_decoder->Decode(
345 descriptor,
346 {.target_width = static_cast<uint32_t>(descriptor->width()),
347 .target_height = static_cast<uint32_t>(descriptor->height())},
348 callback);
349 };
350
351 auto set_up_io_manager_and_decode = [&]() {
352 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
353 runners.GetUITaskRunner()->PostTask(decode_image);
354 };
355
356 runners.GetIOTaskRunner()->PostTask(set_up_io_manager_and_decode);
357 latch.Wait();
358}
359
360TEST_F(ImageDecoderFixtureTest, ImpellerUploadToSharedNoGpu) {
361#if !IMPELLER_SUPPORTS_RENDERING
362 GTEST_SKIP() << "Impeller only test.";
363#endif // IMPELLER_SUPPORTS_RENDERING
364
365 auto no_gpu_access_context =
366 std::make_shared<impeller::TestImpellerContext>();
367 auto gpu_disabled_switch = std::make_shared<fml::SyncSwitch>(true);
368
369 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_8888_SkColorType,
370 SkAlphaType::kPremul_SkAlphaType);
371 ImageDecoderImpeller::ImageInfo decoder_info = {
372 .size = impeller::ISize(10, 10),
374 };
375 auto bitmap = std::make_shared<SkBitmap>();
376 bitmap->allocPixels(info, 10 * 4);
378 desc.size = bitmap->computeByteSize();
379 auto buffer = std::make_shared<impeller::TestImpellerDeviceBuffer>(desc);
380
381 bool invoked = false;
382 auto cb = [&invoked](const sk_sp<DlImage>& image,
383 const std::string& message) { invoked = true; };
384
386 cb, no_gpu_access_context, buffer, decoder_info, std::nullopt,
387 gpu_disabled_switch);
388
389 EXPECT_EQ(no_gpu_access_context->command_buffer_count_, 0ul);
390 EXPECT_FALSE(invoked);
391 EXPECT_EQ(no_gpu_access_context->DidDisposeResources(), false);
392
394 no_gpu_access_context, bitmap);
395
396 ASSERT_EQ(no_gpu_access_context->command_buffer_count_, 0ul);
397 ASSERT_EQ(result.second, "");
398 EXPECT_EQ(no_gpu_access_context->DidDisposeResources(), true);
399 EXPECT_EQ(
400 result.first->impeller_texture()->GetTextureDescriptor().storage_mode,
402
403 no_gpu_access_context->FlushTasks(/*fail=*/true);
404}
405
407 ImpellerUploadToSharedNoGpuTaskFlushingFailure) {
408#if !IMPELLER_SUPPORTS_RENDERING
409 GTEST_SKIP() << "Impeller only test.";
410#endif // IMPELLER_SUPPORTS_RENDERING
411
412 auto no_gpu_access_context =
413 std::make_shared<impeller::TestImpellerContext>();
414 auto gpu_disabled_switch = std::make_shared<fml::SyncSwitch>(true);
415
416 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_8888_SkColorType,
417 SkAlphaType::kPremul_SkAlphaType);
418 ImageDecoderImpeller::ImageInfo decoder_info = {
419 .size = impeller::ISize(10, 10),
421 };
422 auto bitmap = std::make_shared<SkBitmap>();
423 bitmap->allocPixels(info, 10 * 4);
425 desc.size = bitmap->computeByteSize();
426 auto buffer = std::make_shared<impeller::TestImpellerDeviceBuffer>(desc);
427
428 sk_sp<DlImage> image;
429 std::string message;
430 bool invoked = false;
431 auto cb = [&invoked, &image, &message](sk_sp<DlImage> p_image,
432 std::string p_message) {
433 invoked = true;
434 image = std::move(p_image);
435 message = std::move(p_message);
436 };
437
439 cb, no_gpu_access_context, buffer, decoder_info, std::nullopt,
440 gpu_disabled_switch);
441
442 EXPECT_EQ(no_gpu_access_context->command_buffer_count_, 0ul);
443 EXPECT_FALSE(invoked);
444
445 no_gpu_access_context->FlushTasks(/*fail=*/true);
446
447 EXPECT_TRUE(invoked);
448 // Creation of the dl image will still fail with the mocked context.
449 EXPECT_NE(message, "");
450}
451
452TEST_F(ImageDecoderFixtureTest, ImpellerNullColorspace) {
453 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_8888_SkColorType,
454 SkAlphaType::kPremul_SkAlphaType);
455 SkBitmap bitmap;
456 bitmap.allocPixels(info, 10 * 4);
457 auto data = SkData::MakeWithoutCopy(bitmap.getPixels(), 10 * 10 * 4);
458 auto image = SkImages::RasterFromBitmap(bitmap);
459 ASSERT_TRUE(image != nullptr);
460 EXPECT_EQ(SkISize::Make(10, 10), image->dimensions());
461 EXPECT_EQ(nullptr, image->colorSpace());
462
463 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
464 std::move(data), ImageDescriptor::CreateImageInfo(image->imageInfo()),
465 10 * 4);
466
467#if IMPELLER_SUPPORTS_RENDERING
468 std::shared_ptr<impeller::Capabilities> capabilities =
471 .Build();
472 std::shared_ptr<impeller::Allocator> allocator =
473 std::make_shared<impeller::TestImpellerAllocator>();
474 absl::StatusOr<ImageDecoderImpeller::DecompressResult> decompressed =
476 descriptor.get(), {.target_width = 100, .target_height = 100},
477 {100, 100},
478 /*supports_wide_gamut=*/true, capabilities, allocator);
479 ASSERT_TRUE(decompressed.ok());
480 EXPECT_EQ(decompressed->image_info.format,
482#endif // IMPELLER_SUPPORTS_RENDERING
483}
484
485TEST_F(ImageDecoderFixtureTest, ImpellerPixelConversion32F) {
486 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_F32_SkColorType,
487 SkAlphaType::kUnpremul_SkAlphaType);
488 SkBitmap bitmap;
489 bitmap.allocPixels(info, 10 * 16);
490 auto data = SkData::MakeWithoutCopy(bitmap.getPixels(), 10 * 10 * 16);
491 auto image = SkImages::RasterFromBitmap(bitmap);
492
493 ASSERT_TRUE(image != nullptr);
494 EXPECT_EQ(SkISize::Make(10, 10), image->dimensions());
495 EXPECT_EQ(nullptr, image->colorSpace());
496
497 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
498 std::move(data), ImageDescriptor::CreateImageInfo(image->imageInfo()),
499 10 * 16);
500
501#if IMPELLER_SUPPORTS_RENDERING
502 std::shared_ptr<impeller::Capabilities> capabilities =
505 .Build();
506 std::shared_ptr<impeller::Allocator> allocator =
507 std::make_shared<impeller::TestImpellerAllocator>();
508 absl::StatusOr<ImageDecoderImpeller::DecompressResult> decompressed =
510 descriptor.get(), {.target_width = 100, .target_height = 100},
511 {100, 100},
512 /*supports_wide_gamut=*/true, capabilities, allocator);
513
514 ASSERT_TRUE(decompressed.ok());
515 EXPECT_EQ(decompressed->image_info.format,
517#endif // IMPELLER_SUPPORTS_RENDERING
518}
519
520TEST_F(ImageDecoderFixtureTest, ImpellerWideGamutDisplayP3Opaque) {
521 auto data = flutter::testing::OpenFixtureAsSkData("DisplayP3Logo.jpg");
522 auto image = SkCodecs::DeferredImage(SkJpegDecoder::Decode(data, nullptr));
523 ASSERT_TRUE(image != nullptr);
524 ASSERT_EQ(SkISize::Make(100, 100), image->dimensions());
525
526 ImageGeneratorRegistry registry;
527 std::shared_ptr<ImageGenerator> generator =
529 ASSERT_TRUE(generator);
530
531 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
532 std::move(generator));
533
534#if IMPELLER_SUPPORTS_RENDERING
535 std::shared_ptr<impeller::Capabilities> capabilities =
538 .Build();
539 std::shared_ptr<impeller::Allocator> allocator =
540 std::make_shared<impeller::TestImpellerAllocator>();
541 absl::StatusOr<ImageDecoderImpeller::DecompressResult> wide_result =
543 descriptor.get(), {.target_width = 100, .target_height = 100},
544 {100, 100},
545 /*supports_wide_gamut=*/true, capabilities, allocator);
546
547 ASSERT_TRUE(wide_result.ok());
548 ASSERT_EQ(wide_result->image_info.format,
550
551 const uint32_t* pixel_ptr = reinterpret_cast<const uint32_t*>(
552 wide_result->device_buffer->OnGetContents());
553 bool found_deep_red = false;
554 for (int i = 0; i < wide_result->image_info.size.width *
555 wide_result->image_info.size.height;
556 ++i) {
557 uint32_t pixel = *pixel_ptr++;
558 float blue = DecodeBGR10((pixel >> 0) & 0x3ff);
559 float green = DecodeBGR10((pixel >> 10) & 0x3ff);
560 float red = DecodeBGR10((pixel >> 20) & 0x3ff);
561 if (fabsf(red - 1.0931f) < 0.01f && fabsf(green - -0.2268f) < 0.01f &&
562 fabsf(blue - -0.1501f) < 0.01f) {
563 found_deep_red = true;
564 break;
565 }
566 }
567 ASSERT_TRUE(found_deep_red);
568
569 absl::StatusOr<ImageDecoderImpeller::DecompressResult> narrow_result =
571 descriptor.get(), {.target_width = 100, .target_height = 100},
572 {100, 100},
573 /*supports_wide_gamut=*/false, capabilities, allocator);
574
575 ASSERT_TRUE(narrow_result.ok());
576 ASSERT_EQ(narrow_result->image_info.format,
578#endif // IMPELLER_SUPPORTS_RENDERING
579}
580
581TEST_F(ImageDecoderFixtureTest, ImpellerNonWideGamut) {
582 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
583 auto image = SkCodecs::DeferredImage(SkJpegDecoder::Decode(data, nullptr));
584 ASSERT_TRUE(image != nullptr);
585 ASSERT_EQ(SkISize::Make(600, 200), image->dimensions());
586
587 ImageGeneratorRegistry registry;
588 std::shared_ptr<ImageGenerator> generator =
590 ASSERT_TRUE(generator);
591
592 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
593 std::move(generator));
594
595#if IMPELLER_SUPPORTS_RENDERING
596 std::shared_ptr<impeller::Capabilities> capabilities =
599 .Build();
600 std::shared_ptr<impeller::Allocator> allocator =
601 std::make_shared<impeller::TestImpellerAllocator>();
602 absl::StatusOr<ImageDecoderImpeller::DecompressResult> result =
604 descriptor.get(), {.target_width = 600, .target_height = 200},
605 {600, 200},
606 /*supports_wide_gamut=*/true, capabilities, allocator);
607
608 ASSERT_TRUE(result.ok());
609 ASSERT_EQ(result->image_info.format,
611#endif // IMPELLER_SUPPORTS_RENDERING
612}
613
614TEST_F(ImageDecoderFixtureTest, ExifDataIsRespectedOnDecode) {
616 TaskRunners runners(GetCurrentTestName(), // label
617 CreateNewThread("platform"), // platform
618 CreateNewThread("raster"), // raster
619 CreateNewThread("ui"), // ui
620 CreateNewThread("io") // io
621 );
622
624
625 std::unique_ptr<IOManager> io_manager;
626
627 auto release_io_manager = [&]() {
628 io_manager.reset();
629 latch.Signal();
630 };
631
632 SkISize decoded_size = SkISize::MakeEmpty();
633 auto decode_image = [&]() {
634 Settings settings;
635 std::unique_ptr<ImageDecoder> image_decoder = ImageDecoder::Make(
636 settings, runners, loop->GetTaskRunner(),
637 io_manager->GetWeakIOManager(), std::make_shared<fml::SyncSwitch>());
638
639 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
640
641 ASSERT_TRUE(data);
642 ASSERT_GE(data->size(), 0u);
643
644 ImageGeneratorRegistry registry;
645 std::shared_ptr<ImageGenerator> generator =
647 ASSERT_TRUE(generator);
648
649 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
650 std::move(data), std::move(generator));
651
652 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
653 const std::string& decode_error) {
654 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
655 ASSERT_TRUE(image && image->skia_image());
656 decoded_size = image->skia_image()->dimensions();
657 runners.GetIOTaskRunner()->PostTask(release_io_manager);
658 };
659 image_decoder->Decode(
660 descriptor,
661 {.target_width = static_cast<uint32_t>(descriptor->width()),
662 .target_height = static_cast<uint32_t>(descriptor->height())},
663 callback);
664 };
665
666 auto set_up_io_manager_and_decode = [&]() {
667 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
668 runners.GetUITaskRunner()->PostTask(decode_image);
669 };
670
671 runners.GetIOTaskRunner()->PostTask(set_up_io_manager_and_decode);
672
673 latch.Wait();
674
675 ASSERT_EQ(decoded_size.width(), 600);
676 ASSERT_EQ(decoded_size.height(), 200);
677}
678
679TEST_F(ImageDecoderFixtureTest, CanDecodeWithoutAGPUContext) {
681 TaskRunners runners(GetCurrentTestName(), // label
682 CreateNewThread("platform"), // platform
683 CreateNewThread("raster"), // raster
684 CreateNewThread("ui"), // ui
685 CreateNewThread("io") // io
686 );
687
689
690 std::unique_ptr<IOManager> io_manager;
691
692 auto release_io_manager = [&]() {
693 io_manager.reset();
694 latch.Signal();
695 };
696
697 auto decode_image = [&]() {
698 Settings settings;
699 std::unique_ptr<ImageDecoder> image_decoder = ImageDecoder::Make(
700 settings, runners, loop->GetTaskRunner(),
701 io_manager->GetWeakIOManager(), std::make_shared<fml::SyncSwitch>());
702
703 auto data = flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg");
704
705 ASSERT_TRUE(data);
706 ASSERT_GE(data->size(), 0u);
707
708 ImageGeneratorRegistry registry;
709 std::shared_ptr<ImageGenerator> generator =
711 ASSERT_TRUE(generator);
712
713 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
714 std::move(data), std::move(generator));
715
716 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
717 const std::string& decode_error) {
718 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
719 ASSERT_TRUE(image && image->skia_image());
720 runners.GetIOTaskRunner()->PostTask(release_io_manager);
721 };
722 image_decoder->Decode(
723 descriptor,
724 {.target_width = static_cast<uint32_t>(descriptor->width()),
725 .target_height = static_cast<uint32_t>(descriptor->height())},
726 callback);
727 };
728
729 auto set_up_io_manager_and_decode = [&]() {
730 io_manager =
731 std::make_unique<TestIOManager>(runners.GetIOTaskRunner(), false);
732 runners.GetUITaskRunner()->PostTask(decode_image);
733 };
734
735 runners.GetIOTaskRunner()->PostTask(set_up_io_manager_and_decode);
736
737 latch.Wait();
738}
739
740TEST_F(ImageDecoderFixtureTest, CanDecodeWithResizes) {
741 const auto image_dimensions =
742 SkJpegDecoder::Decode(
743 flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg"),
744 nullptr)
745 ->dimensions();
746
747 ASSERT_FALSE(image_dimensions.isEmpty());
748
749 ASSERT_NE(image_dimensions.width(), image_dimensions.height());
750
752 TaskRunners runners(GetCurrentTestName(), // label
753 CreateNewThread("platform"), // platform
754 CreateNewThread("raster"), // raster
755 CreateNewThread("ui"), // ui
756 CreateNewThread("io") // io
757 );
758
760 std::unique_ptr<IOManager> io_manager;
761 std::unique_ptr<ImageDecoder> image_decoder;
762
763 // Setup the IO manager.
764 PostTaskSync(runners.GetIOTaskRunner(), [&]() {
765 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
766 });
767
768 // Setup the image decoder.
769 PostTaskSync(runners.GetUITaskRunner(), [&]() {
770 Settings settings;
771 image_decoder = ImageDecoder::Make(settings, runners, loop->GetTaskRunner(),
772 io_manager->GetWeakIOManager(),
773 std::make_shared<fml::SyncSwitch>());
774 });
775
776 // Setup a generic decoding utility that gives us the final decoded size.
777 auto decoded_size = [&](uint32_t target_width,
778 uint32_t target_height) -> SkISize {
779 SkISize final_size = SkISize::MakeEmpty();
780 runners.GetUITaskRunner()->PostTask([&]() {
781 auto data = flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg");
782
783 ASSERT_TRUE(data);
784 ASSERT_GE(data->size(), 0u);
785
786 ImageGeneratorRegistry registry;
787 std::shared_ptr<ImageGenerator> generator =
789 ASSERT_TRUE(generator);
790
791 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
792 std::move(data), std::move(generator));
793
795 [&](const sk_sp<DlImage>& image, const std::string& decode_error) {
796 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
797 ASSERT_TRUE(image && image->skia_image());
798 final_size = image->skia_image()->dimensions();
799 latch.Signal();
800 };
801 image_decoder->Decode(
802 descriptor,
803 {.target_width = target_width, .target_height = target_height},
804 callback);
805 });
806 latch.Wait();
807 return final_size;
808 };
809
810 ASSERT_EQ(SkISize::Make(3024, 4032), image_dimensions);
811 ASSERT_EQ(decoded_size(3024, 4032), image_dimensions);
812 ASSERT_EQ(decoded_size(100, 100), SkISize::Make(100, 100));
813
814 // Destroy the IO manager
815 PostTaskSync(runners.GetIOTaskRunner(), [&]() { io_manager.reset(); });
816
817 // Destroy the image decoder
818 PostTaskSync(runners.GetUITaskRunner(), [&]() { image_decoder.reset(); });
819}
820
821// Verifies https://skia-review.googlesource.com/c/skia/+/259161 is present in
822// Flutter.
823TEST(ImageDecoderTest,
824 VerifyCodecRepeatCountsForGifAndWebPAreConsistentWithLoopCounts) {
825 auto gif_mapping = flutter::testing::OpenFixtureAsSkData("hello_loop_2.gif");
826 auto webp_mapping =
827 flutter::testing::OpenFixtureAsSkData("hello_loop_2.webp");
828
829 ASSERT_TRUE(gif_mapping);
830 ASSERT_TRUE(webp_mapping);
831
832 ImageGeneratorRegistry registry;
833
834 auto gif_generator = registry.CreateCompatibleGenerator(gif_mapping);
835 auto webp_generator = registry.CreateCompatibleGenerator(webp_mapping);
836
837 ASSERT_TRUE(gif_generator);
838 ASSERT_TRUE(webp_generator);
839
840 // Both fixtures have a loop count of 2.
841 ASSERT_EQ(gif_generator->GetPlayCount(), static_cast<unsigned int>(2));
842 ASSERT_EQ(webp_generator->GetPlayCount(), static_cast<unsigned int>(2));
843}
844
845TEST(ImageDecoderTest, VerifySimpleDecoding) {
846 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
847 auto codec = SkJpegDecoder::Decode(data, nullptr);
848 ASSERT_TRUE(codec != nullptr);
849 auto image = SkCodecs::DeferredImage(std::move(codec));
850 ASSERT_TRUE(image != nullptr);
851 EXPECT_EQ(600, image->width());
852 EXPECT_EQ(200, image->height());
853
854 ImageGeneratorRegistry registry;
855 std::shared_ptr<ImageGenerator> generator =
857 ASSERT_TRUE(generator);
858
859 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
860 std::move(generator));
861 auto compressed_image = ImageDecoderSkia::ImageFromCompressedData(
862 descriptor.get(), 6, 2, fml::tracing::TraceFlow(""));
863 EXPECT_EQ(compressed_image->width(), 6);
864 EXPECT_EQ(compressed_image->height(), 2);
865 EXPECT_EQ(compressed_image->alphaType(), kOpaque_SkAlphaType);
866
867#if IMPELLER_SUPPORTS_RENDERING
868 std::shared_ptr<impeller::Capabilities> capabilities =
871 .Build();
872 std::shared_ptr<impeller::Capabilities> capabilities_no_blit =
875 .Build();
876 // Bitmap sizes reflect the original image size as resizing is done on the
877 // GPU if the src size is smaller than the max texture size.
878 std::shared_ptr<impeller::Allocator> allocator =
879 std::make_shared<impeller::TestImpellerAllocator>();
881 descriptor.get(), {.target_width = 6, .target_height = 2}, {1000, 1000},
882 /*supports_wide_gamut=*/false, capabilities, allocator);
883 ASSERT_TRUE(result_1.ok());
884 EXPECT_EQ(result_1->image_info.size.width, 75);
885 EXPECT_EQ(result_1->image_info.size.height, 25);
886
887 // Bitmap sizes reflect the scaled size if the source size is larger than
888 // max texture size even if destination size isn't max texture size.
890 descriptor.get(), {.target_width = 6, .target_height = 2}, {10, 10},
891 /*supports_wide_gamut=*/false, capabilities, allocator);
892 ASSERT_TRUE(result_2.ok());
893 EXPECT_EQ(result_2->image_info.size.width, 6);
894 EXPECT_EQ(result_2->image_info.size.height, 2);
895
896 // If the destination size is larger than the max texture size the image
897 // is scaled down.
899 descriptor.get(), {.target_width = 60, .target_height = 20}, {10, 10},
900 /*supports_wide_gamut=*/false, capabilities, allocator);
901 ASSERT_TRUE(result_3.ok());
902 EXPECT_EQ(result_3->image_info.size.width, 10);
903 EXPECT_EQ(result_3->image_info.size.height, 10);
904
905 // CPU resize is forced.
907 descriptor.get(), {.target_width = 6, .target_height = 2}, {1000, 1000},
908 /*supports_wide_gamut=*/false, capabilities_no_blit, allocator);
909 ASSERT_TRUE(result_4.ok());
910 EXPECT_EQ(result_4->image_info.size.width, 6);
911 EXPECT_EQ(result_4->image_info.size.height, 2);
912#endif // IMPELLER_SUPPORTS_RENDERING
913}
914
915TEST(ImageDecoderTest, ImagesWithTransparencyArePremulAlpha) {
916 auto data = flutter::testing::OpenFixtureAsSkData("heart_end.png");
917 ASSERT_TRUE(data);
918 ImageGeneratorRegistry registry;
919 std::shared_ptr<ImageGenerator> generator =
921 ASSERT_TRUE(generator);
922
923 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
924 std::move(generator));
925 auto compressed_image = ImageDecoderSkia::ImageFromCompressedData(
926 descriptor.get(), 250, 250, fml::tracing::TraceFlow(""));
927 ASSERT_TRUE(compressed_image);
928 ASSERT_EQ(compressed_image->width(), 250);
929 ASSERT_EQ(compressed_image->height(), 250);
930 ASSERT_EQ(compressed_image->alphaType(), kPremul_SkAlphaType);
931}
932
933TEST(ImageDecoderTest, VerifySubpixelDecodingPreservesExifOrientation) {
934 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
935
936 ImageGeneratorRegistry registry;
937 std::shared_ptr<ImageGenerator> generator =
939 ASSERT_TRUE(generator);
940 auto descriptor =
941 fml::MakeRefCounted<ImageDescriptor>(data, std::move(generator));
942
943 // If Exif metadata is ignored, the height and width will be swapped because
944 // "Rotate 90 CW" is what is encoded there.
945 ASSERT_EQ(600, descriptor->width());
946 ASSERT_EQ(200, descriptor->height());
947
948 auto image = SkCodecs::DeferredImage(SkJpegDecoder::Decode(data, nullptr));
949 ASSERT_TRUE(image != nullptr);
950 ASSERT_EQ(600, image->width());
951 ASSERT_EQ(200, image->height());
952
953 auto decode = [descriptor](uint32_t target_width, uint32_t target_height) {
955 descriptor.get(), target_width, target_height,
957 };
958
959 auto expected_data = flutter::testing::OpenFixtureAsSkData("Horizontal.png");
960 ASSERT_TRUE(expected_data != nullptr);
961 ASSERT_FALSE(expected_data->isEmpty());
962
963 auto assert_image = [&](const auto& decoded_image,
964 const std::string& decode_error) {
965 ASSERT_EQ(decoded_image->dimensions(), SkISize::Make(300, 100));
966 sk_sp<SkData> encoded =
967 SkPngEncoder::Encode(nullptr, decoded_image.get(), {});
968 ASSERT_TRUE(encoded->equals(expected_data.get()));
969 };
970
971 assert_image(decode(300, 100), {});
972}
973
975 MultiFrameCodecCanBeCollectedBeforeIOTasksFinish) {
976 // This test verifies that the MultiFrameCodec safely shares state between
977 // tasks on the IO and UI runners, and does not allow unsafe memory access if
978 // the UI object is collected while the IO thread still has pending decode
979 // work. This could happen in a real application if the engine is collected
980 // while a multi-frame image is decoding. To exercise this, the test:
981 // - Starts a Dart VM
982 // - Latches the IO task runner
983 // - Create a MultiFrameCodec for an animated gif pointed to a callback
984 // in the Dart fixture
985 // - Calls getNextFrame on the UI task runner
986 // - Collects the MultiFrameCodec object before unlatching the IO task
987 // runner.
988 // - Unlatches the IO task runner
989 auto settings = CreateSettingsForFixture();
990 auto vm_ref = DartVMRef::Create(settings);
991 auto vm_data = vm_ref.GetVMData();
992
993 auto gif_mapping = flutter::testing::OpenFixtureAsSkData("hello_loop_2.gif");
994
995 ASSERT_TRUE(gif_mapping);
996
997 ImageGeneratorRegistry registry;
998 std::shared_ptr<ImageGenerator> gif_generator =
999 registry.CreateCompatibleGenerator(gif_mapping);
1000 ASSERT_TRUE(gif_generator);
1001
1002 TaskRunners runners(GetCurrentTestName(), // label
1003 CreateNewThread("platform"), // platform
1004 CreateNewThread("raster"), // raster
1005 CreateNewThread("ui"), // ui
1006 CreateNewThread("io") // io
1007 );
1008
1010 std::unique_ptr<TestIOManager> io_manager;
1011
1012 // Setup the IO manager.
1013 PostTaskSync(runners.GetIOTaskRunner(), [&]() {
1014 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
1015 });
1016
1017 auto isolate = RunDartCodeInIsolate(vm_ref, settings, runners, "main", {},
1019 io_manager->GetWeakIOManager());
1020
1021 // Latch the IO task runner.
1022 runners.GetIOTaskRunner()->PostTask([&]() { io_latch.Wait(); });
1023
1024 PostTaskSync(runners.GetUITaskRunner(), [&]() {
1025 fml::AutoResetWaitableEvent isolate_latch;
1026 fml::RefPtr<MultiFrameCodec> codec;
1027 EXPECT_TRUE(isolate->RunInIsolateScope([&]() -> bool {
1028 Dart_Handle library = Dart_RootLibrary();
1029 if (Dart_IsError(library)) {
1030 isolate_latch.Signal();
1031 return false;
1032 }
1033 Dart_Handle closure =
1034 Dart_GetField(library, Dart_NewStringFromCString("frameCallback"));
1035 if (Dart_IsError(closure) || !Dart_IsClosure(closure)) {
1036 isolate_latch.Signal();
1037 return false;
1038 }
1039
1040 codec = fml::MakeRefCounted<MultiFrameCodec>(std::move(gif_generator));
1041 codec->getNextFrame(closure);
1042 codec = nullptr;
1043 isolate_latch.Signal();
1044 return true;
1045 }));
1046 isolate_latch.Wait();
1047
1048 EXPECT_FALSE(codec);
1049
1050 io_latch.Signal();
1051 });
1052
1053 // Destroy the IO manager
1054 PostTaskSync(runners.GetIOTaskRunner(), [&]() { io_manager.reset(); });
1055}
1056
1058 auto context = std::make_shared<impeller::TestImpellerContext>();
1059 auto allocator = ImpellerAllocator(context->GetResourceAllocator());
1060
1061 EXPECT_FALSE(allocator.allocPixelRef(nullptr));
1062}
1063
1064} // namespace testing
1065} // namespace flutter
1066
1067// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
1068// NOLINTEND(clang-analyzer-core.StackAddressEscape)
static DartVMRef Create(const Settings &settings, fml::RefPtr< const DartSnapshot > vm_snapshot=nullptr, fml::RefPtr< const DartSnapshot > isolate_snapshot=nullptr)
std::function< void(sk_sp< DlImage >, std::string)> ImageResult
static std::unique_ptr< ImageDecoder > Make(const Settings &settings, const TaskRunners &runners, std::shared_ptr< fml::ConcurrentTaskRunner > concurrent_task_runner, const fml::WeakPtr< IOManager > &io_manager, const std::shared_ptr< fml::SyncSwitch > &gpu_disabled_switch)
static absl::StatusOr< DecompressResult > DecompressTexture(ImageDescriptor *descriptor, const ImageDecoder::Options &options, impeller::ISize max_texture_size, bool supports_wide_gamut, const std::shared_ptr< const impeller::Capabilities > &capabilities, const std::shared_ptr< impeller::Allocator > &allocator)
static void UploadTextureToPrivate(ImageResult result, const std::shared_ptr< impeller::Context > &context, const std::shared_ptr< impeller::DeviceBuffer > &buffer, const ImageInfo &image_info, const std::optional< SkImageInfo > &resize_info, const std::shared_ptr< const fml::SyncSwitch > &gpu_disabled_switch)
Create a device private texture from the provided host buffer.
static std::pair< sk_sp< DlImage >, std::string > UploadTextureToStorage(const std::shared_ptr< impeller::Context > &context, std::shared_ptr< SkBitmap > bitmap)
Create a texture from the provided bitmap.
static sk_sp< SkImage > ImageFromCompressedData(ImageDescriptor *descriptor, uint32_t target_width, uint32_t target_height, const fml::tracing::TraceFlow &flow)
static ImageInfo CreateImageInfo(const SkImageInfo &sk_image_info)
The minimal interface necessary for defining a decoder that can be used for both single and multi-fra...
Keeps a priority-ordered registry of image generator builders to be used when decoding images....
std::shared_ptr< ImageGenerator > CreateCompatibleGenerator(const sk_sp< SkData > &buffer)
Walks the list of image generator builders in descending priority order until a compatible ImageGener...
fml::RefPtr< fml::TaskRunner > GetUITaskRunner() const
fml::RefPtr< fml::TaskRunner > GetIOTaskRunner() const
fml::RefPtr< flutter::SkiaUnrefQueue > GetSkiaUnrefQueue() const override
std::shared_ptr< const fml::SyncSwitch > GetIsGpuDisabledSyncSwitch() override
fml::WeakPtr< IOManager > GetWeakIOManager() const override
std::shared_ptr< impeller::Context > GetImpellerContext() const override
Retrieve the impeller::Context.
TestIOManager(const fml::RefPtr< fml::TaskRunner > &task_runner, bool has_gpu_context=true)
fml::WeakPtr< GrDirectContext > GetResourceContext() const override
An Image generator that pretends it can't recognize the data it was given.
const SkImageInfo & GetInfo()
Returns basic information about the contents of the encoded image. This information can almost always...
unsigned int GetPlayCount() const
The number of times an animated image should play through before playback stops.
const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index)
Get information about a single frame in the context of a multi-frame image, useful for animation and ...
bool GetPixels(const SkImageInfo &info, void *pixels, size_t row_bytes, unsigned int frame_index, std::optional< unsigned int > prior_frame)
Decode the image into a given buffer. This method is currently always used for sub-pixel image decodi...
SkISize GetScaledDimensions(float scale)
Given a scale value, find the closest image size that can be used for efficiently decoding the image....
unsigned int GetFrameCount() const
Get the number of frames that the encoded image stores. This method is always expected to be called b...
static std::shared_ptr< ConcurrentMessageLoop > Create(size_t worker_count=std::thread::hardware_concurrency())
static void RunNowOrPostTask(const fml::RefPtr< fml::TaskRunner > &runner, const fml::closure &task)
virtual void PostTask(const fml::closure &task) override
virtual bool RunsTasksOnCurrentThread()
CapabilitiesBuilder & SetSupportsTextureToTextureBlits(bool value)
std::unique_ptr< Capabilities > Build()
To do anything rendering related with Impeller, you need a context.
Definition context.h:65
std::shared_ptr< CommandQueue > GetCommandQueue() const override
Return the graphics queue for submitting command buffers.
std::shared_ptr< ShaderLibrary > GetShaderLibrary() const override
Returns the library of shaders used to specify the programmable stages of a pipeline.
RuntimeStageBackend GetRuntimeStageBackend() const override
Retrieve the runtime stage for this context type.
std::shared_ptr< PipelineLibrary > GetPipelineLibrary() const override
Returns the library of pipelines used by render or compute commands.
void Shutdown() override
Force all pending asynchronous work to finish. This is achieved by deleting all owned concurrent mess...
std::shared_ptr< Allocator > GetResourceAllocator() const override
Returns the allocator used to create textures and buffers on the device.
bool IsValid() const override
Determines if a context is valid. If the caller ever receives an invalid context, they must discard i...
const std::shared_ptr< const Capabilities > & GetCapabilities() const override
Get the capabilities of Impeller context. All optionally supported feature of the platform,...
std::string DescribeGpuModel() const override
BackendType GetBackendType() const override
Get the graphics backend of an Impeller context.
std::shared_ptr< CommandBuffer > CreateCommandBuffer() const override
Create a new command buffer. Command buffers can be used to encode graphics, blit,...
void StoreTaskForGPU(const std::function< void()> &task, const std::function< void()> &failure) override
std::shared_ptr< SamplerLibrary > GetSamplerLibrary() const override
Returns the library of combined image samplers used in shaders.
FlutterVulkanImage * image
VkQueue queue
Definition main.cc:71
const char * message
FlutterDesktopBinaryReply callback
#define FML_CHECK(condition)
Definition logging.h:104
#define FML_UNREACHABLE()
Definition logging.h:128
#define FML_DISALLOW_COPY_AND_ASSIGN(TypeName)
Definition macros.h:27
std::shared_ptr< SkBitmap > bitmap
std::shared_ptr< ImpellerAllocator > allocator
std::string GetCurrentTestName()
Gets the name of the currently running test. This is useful in generating logs or assets based on tes...
Definition testing.cc:14
TEST_F(DisplayListTest, Defaults)
void PostTaskSync(const fml::RefPtr< fml::TaskRunner > &task_runner, const std::function< void()> &function)
std::string GetDefaultKernelFilePath()
Returns the default path to kernel_blob.bin. This file is within the directory returned by GetFixture...
Definition testing.cc:18
std::unique_ptr< AutoIsolateShutdown > RunDartCodeInIsolate(DartVMRef &vm_ref, const Settings &settings, const TaskRunners &task_runners, std::string entrypoint, const std::vector< std::string > &args, const std::string &kernel_file_path, fml::WeakPtr< IOManager > io_manager, std::unique_ptr< PlatformConfiguration > platform_configuration)
sk_sp< SkData > OpenFixtureAsSkData(const std::string &fixture_name)
Opens a fixture of the given file name and returns a Skia SkData holding its contents.
Definition testing.cc:63
TEST(NativeAssetsManagerTest, NoAvailableAssets)
it will be possible to load the file into Perfetto s trace viewer use test Running tests that layout and measure text will not yield consistent results across various platforms Enabling this option will make font resolution default to the Ahem test font on all disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font manager
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot data
Definition switch_defs.h:36
TEST_F(EngineAnimatorTest, AnimatorAcceptsMultipleRenders)
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set profile Make the profiler discard new samples once the profiler sample buffer is full When this flag is not the profiler sample buffer is used as a ring buffer
Definition switch_defs.h:98
ISize64 ISize
Definition size.h:162
Definition ref_ptr.h:261
Info about a single frame in the context of a multi-frame image, useful for animation and blending.