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
28#include "fml/logging.h"
31#include "third_party/skia/include/codec/SkCodec.h"
32#include "third_party/skia/include/codec/SkCodecAnimation.h"
33#include "third_party/skia/include/codec/SkJpegDecoder.h"
34#include "third_party/skia/include/core/SkData.h"
35#include "third_party/skia/include/core/SkImage.h"
36#include "third_party/skia/include/core/SkImageInfo.h"
37#include "third_party/skia/include/core/SkSize.h"
38#include "third_party/skia/include/encode/SkPngEncoder.h"
39
40// CREATE_NATIVE_ENTRY is leaky by design
41// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
42// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
43
44namespace impeller {
45
47 public:
49
50 BackendType GetBackendType() const override { return BackendType::kMetal; }
51
52 std::string DescribeGpuModel() const override { return "TestGpu"; }
53
54 bool IsValid() const override { return true; }
55
56 const std::shared_ptr<const Capabilities>& GetCapabilities() const override {
57 return capabilities_;
58 }
59
60 std::shared_ptr<Allocator> GetResourceAllocator() const override {
61 return std::make_shared<TestImpellerAllocator>();
62 }
63
64 std::shared_ptr<ShaderLibrary> GetShaderLibrary() const override {
65 return nullptr;
66 }
67
68 std::shared_ptr<SamplerLibrary> GetSamplerLibrary() const override {
69 return nullptr;
70 }
71
72 std::shared_ptr<PipelineLibrary> GetPipelineLibrary() const override {
73 return nullptr;
74 }
75
76 std::shared_ptr<CommandQueue> GetCommandQueue() const override {
78 }
79
80 std::shared_ptr<CommandBuffer> CreateCommandBuffer() const override {
82 return nullptr;
83 }
84
85 // A stub returning false is allowed from implementations that are not
86 // planned to be used in benchmarking situations.
87 bool FinishQueue() override { return false; }
88
89 void StoreTaskForGPU(const std::function<void()>& task,
90 const std::function<void()>& failure) override {
91 tasks_.push_back(PendingTask{task, failure});
92 }
93
94 void FlushTasks(bool fail = false) {
95 for (auto& task : tasks_) {
96 if (fail) {
97 task.task();
98 } else {
99 task.failure();
100 }
101 }
102 tasks_.clear();
103 }
104
105 void DisposeThreadLocalCachedResources() override { did_dispose_ = true; }
106
107 void Shutdown() override {}
108
112
113 bool DidDisposeResources() const { return did_dispose_; }
114
115 mutable size_t command_buffer_count_ = 0;
116
117 private:
118 struct PendingTask {
119 std::function<void()> task;
120 std::function<void()> failure;
121 };
122 std::vector<PendingTask> tasks_;
123 std::shared_ptr<const Capabilities> capabilities_;
124 bool did_dispose_ = false;
125};
126
127} // namespace impeller
128
129namespace flutter {
130namespace testing {
131
132class TestIOManager final : public IOManager {
133 public:
134 explicit TestIOManager(const fml::RefPtr<fml::TaskRunner>& task_runner,
135 bool has_gpu_context = true)
136 : gl_surface_(DlISize(1, 1)),
137 impeller_context_(std::make_shared<impeller::TestImpellerContext>()),
138 gl_context_(has_gpu_context ? gl_surface_.CreateGrContext() : nullptr),
139 weak_gl_context_factory_(
140 has_gpu_context
141 ? std::make_unique<fml::WeakPtrFactory<GrDirectContext>>(
142 gl_context_.get())
143 : nullptr),
144 unref_queue_(fml::MakeRefCounted<SkiaUnrefQueue>(
145 task_runner,
146 fml::TimeDelta::FromNanoseconds(0),
147 gl_context_)),
148 runner_(task_runner),
149 is_gpu_disabled_sync_switch_(std::make_shared<fml::SyncSwitch>()),
150 weak_factory_(this) {
151 FML_CHECK(task_runner->RunsTasksOnCurrentThread())
152 << "The IO manager must be initialized its primary task runner. The "
153 "test harness may not be set up correctly/safely.";
154 weak_prototype_ = weak_factory_.GetWeakPtr();
155 }
156
157 ~TestIOManager() override {
160 [&latch, queue = unref_queue_]() {
161 queue->Drain();
162 latch.Signal();
163 });
164 latch.Wait();
165 }
166
167 // |IOManager|
169 return weak_prototype_;
170 }
171
172 // |IOManager|
174 return weak_gl_context_factory_ ? weak_gl_context_factory_->GetWeakPtr()
176 }
177
178 // |IOManager|
180 return unref_queue_;
181 }
182
183 // |IOManager|
184 std::shared_ptr<const fml::SyncSwitch> GetIsGpuDisabledSyncSwitch() override {
186 return is_gpu_disabled_sync_switch_;
187 }
188
189 // |IOManager|
190 std::shared_ptr<impeller::Context> GetImpellerContext() const override {
191 return impeller_context_;
192 }
193
194 void SetGpuDisabled(bool disabled) {
195 is_gpu_disabled_sync_switch_->SetSwitch(disabled);
196 }
197
199
200 private:
201 TestGLSurface gl_surface_;
202 std::shared_ptr<impeller::Context> impeller_context_;
203 sk_sp<GrDirectContext> gl_context_;
204 std::unique_ptr<fml::WeakPtrFactory<GrDirectContext>>
205 weak_gl_context_factory_;
206 fml::RefPtr<SkiaUnrefQueue> unref_queue_;
207 fml::WeakPtr<TestIOManager> weak_prototype_;
209 std::shared_ptr<fml::SyncSwitch> is_gpu_disabled_sync_switch_;
211
213};
214
216
217TEST_F(ImageDecoderFixtureTest, CanCreateImageDecoder) {
219 auto thread_task_runner = CreateNewThread();
220 TaskRunners runners(GetCurrentTestName(), // label
221 thread_task_runner, // platform
222 thread_task_runner, // raster
223 thread_task_runner, // ui
224 thread_task_runner // io
225
226 );
227
228 PostTaskSync(runners.GetIOTaskRunner(), [&]() {
229 TestIOManager manager(runners.GetIOTaskRunner());
230 Settings settings;
231 auto decoder = ImageDecoder::Make(settings, runners, loop->GetTaskRunner(),
232 manager.GetWeakIOManager(),
233 std::make_shared<fml::SyncSwitch>());
234 ASSERT_NE(decoder, nullptr);
235 });
236}
237
238/// An Image generator that pretends it can't recognize the data it was given.
240 public:
241 UnknownImageGenerator() : info_(SkImageInfo::MakeUnknown()) {};
243 const SkImageInfo& GetInfo() { return info_; }
244
245 unsigned int GetFrameCount() const { return 1; }
246
247 unsigned int GetPlayCount() const { return 1; }
248
249 const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) {
250 return {std::nullopt, 0, SkCodecAnimation::DisposalMethod::kKeep};
251 }
252
253 SkISize GetScaledDimensions(float scale) {
254 return SkISize::Make(info_.width(), info_.height());
255 }
256
257 bool GetPixels(const SkImageInfo& info,
258 void* pixels,
259 size_t row_bytes,
260 unsigned int frame_index,
261 std::optional<unsigned int> prior_frame) {
262 return false;
263 };
264
265 private:
266 SkImageInfo info_;
267};
268
269TEST_F(ImageDecoderFixtureTest, InvalidImageResultsError) {
271 auto thread_task_runner = CreateNewThread();
272 TaskRunners runners(GetCurrentTestName(), // label
273 thread_task_runner, // platform
274 thread_task_runner, // raster
275 thread_task_runner, // ui
276 thread_task_runner // io
277 );
278
280 thread_task_runner->PostTask([&]() {
282 Settings settings;
283 auto decoder = ImageDecoder::Make(settings, runners, loop->GetTaskRunner(),
284 manager.GetWeakIOManager(),
285 std::make_shared<fml::SyncSwitch>());
286
287 auto data = flutter::testing::OpenFixtureAsSkData("ThisDoesNotExist.jpg");
288 ASSERT_FALSE(data);
289
290 fml::RefPtr<ImageDescriptor> image_descriptor =
291 fml::MakeRefCounted<ImageDescriptor>(
292 std::move(data), std::make_unique<UnknownImageGenerator>());
293
294 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
295 const std::string& decode_error) {
296 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
297 ASSERT_FALSE(image);
298 latch.Signal();
299 };
300 decoder->Decode(image_descriptor, {.target_width = 0, .target_height = 0},
301 callback);
302 });
303 latch.Wait();
304}
305
306TEST_F(ImageDecoderFixtureTest, ValidImageResultsInSuccess) {
308 TaskRunners runners(GetCurrentTestName(), // label
309 CreateNewThread("platform"), // platform
310 CreateNewThread("raster"), // raster
311 CreateNewThread("ui"), // ui
312 CreateNewThread("io") // io
313 );
314
316
317 std::unique_ptr<TestIOManager> io_manager;
318
319 auto release_io_manager = [&]() {
320 io_manager.reset();
321 latch.Signal();
322 };
323 auto decode_image = [&]() {
324 Settings settings;
325 std::unique_ptr<ImageDecoder> image_decoder = ImageDecoder::Make(
326 settings, runners, loop->GetTaskRunner(),
327 io_manager->GetWeakIOManager(), std::make_shared<fml::SyncSwitch>());
328
329 auto data = flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg");
330
331 ASSERT_TRUE(data);
332 ASSERT_GE(data->size(), 0u);
333
334 ImageGeneratorRegistry registry;
335 std::shared_ptr<ImageGenerator> generator =
337 ASSERT_TRUE(generator);
338
339 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
340 std::move(data), std::move(generator));
341
342 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
343 const std::string& decode_error) {
344 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
345 auto skia_image = image ? image->asSkiaImage() : nullptr;
346 ASSERT_TRUE(skia_image && skia_image->skia_image());
347 EXPECT_TRUE(io_manager->did_access_is_gpu_disabled_sync_switch_);
348 runners.GetIOTaskRunner()->PostTask(release_io_manager);
349 };
350
351 EXPECT_FALSE(io_manager->did_access_is_gpu_disabled_sync_switch_);
352 image_decoder->Decode(
353 descriptor,
354 {.target_width = static_cast<uint32_t>(descriptor->width()),
355 .target_height = static_cast<uint32_t>(descriptor->height())},
356 callback);
357 };
358
359 auto set_up_io_manager_and_decode = [&]() {
360 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
361 runners.GetUITaskRunner()->PostTask(decode_image);
362 };
363
364 runners.GetIOTaskRunner()->PostTask(set_up_io_manager_and_decode);
365 latch.Wait();
366}
367
368TEST_F(ImageDecoderFixtureTest, ImpellerUploadToSharedNoGpu) {
369#if !IMPELLER_SUPPORTS_RENDERING
370 GTEST_SKIP() << "Impeller only test.";
371#endif // IMPELLER_SUPPORTS_RENDERING
372
373 auto no_gpu_access_context =
374 std::make_shared<impeller::TestImpellerContext>();
375 auto gpu_disabled_switch = std::make_shared<fml::SyncSwitch>(true);
376
377 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_8888_SkColorType,
378 SkAlphaType::kPremul_SkAlphaType);
379 ImageDecoderImpeller::ImageInfo decoder_info = {
380 .size = impeller::ISize(10, 10),
382 };
383 auto bitmap = std::make_shared<SkBitmap>();
384 bitmap->allocPixels(info, 10 * 4);
386 desc.size = bitmap->computeByteSize();
387 auto buffer = std::make_shared<impeller::TestImpellerDeviceBuffer>(desc);
388
389 bool invoked = false;
390 auto cb = [&invoked](const sk_sp<DlImage>& image,
391 const std::string& message) { invoked = true; };
392
394 cb, no_gpu_access_context, buffer, decoder_info, std::nullopt,
395 gpu_disabled_switch);
396
397 EXPECT_EQ(no_gpu_access_context->command_buffer_count_, 0ul);
398 EXPECT_FALSE(invoked);
399 EXPECT_EQ(no_gpu_access_context->DidDisposeResources(), false);
400
402 no_gpu_access_context, bitmap);
403
404 ASSERT_EQ(no_gpu_access_context->command_buffer_count_, 0ul);
405 ASSERT_EQ(result.second, "");
406 EXPECT_EQ(no_gpu_access_context->DidDisposeResources(), true);
407 EXPECT_EQ(result.first->asImpellerImage()
408 ->GetImpellerTexture(no_gpu_access_context)
409 ->GetTextureDescriptor()
410 .storage_mode,
412
413 no_gpu_access_context->FlushTasks(/*fail=*/true);
414}
415
417 ImpellerUploadToSharedNoGpuTaskFlushingFailure) {
418#if !IMPELLER_SUPPORTS_RENDERING
419 GTEST_SKIP() << "Impeller only test.";
420#endif // IMPELLER_SUPPORTS_RENDERING
421
422 auto no_gpu_access_context =
423 std::make_shared<impeller::TestImpellerContext>();
424 auto gpu_disabled_switch = std::make_shared<fml::SyncSwitch>(true);
425
426 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_8888_SkColorType,
427 SkAlphaType::kPremul_SkAlphaType);
428 ImageDecoderImpeller::ImageInfo decoder_info = {
429 .size = impeller::ISize(10, 10),
431 };
432 auto bitmap = std::make_shared<SkBitmap>();
433 bitmap->allocPixels(info, 10 * 4);
435 desc.size = bitmap->computeByteSize();
436 auto buffer = std::make_shared<impeller::TestImpellerDeviceBuffer>(desc);
437
438 sk_sp<DlImage> image;
439 std::string message;
440 bool invoked = false;
441 auto cb = [&invoked, &image, &message](sk_sp<DlImage> p_image,
442 std::string p_message) {
443 invoked = true;
444 image = std::move(p_image);
445 message = std::move(p_message);
446 };
447
449 cb, no_gpu_access_context, buffer, decoder_info, std::nullopt,
450 gpu_disabled_switch);
451
452 EXPECT_EQ(no_gpu_access_context->command_buffer_count_, 0ul);
453 EXPECT_FALSE(invoked);
454
455 no_gpu_access_context->FlushTasks(/*fail=*/true);
456
457 EXPECT_TRUE(invoked);
458 // Creation of the dl image will still fail with the mocked context.
459 EXPECT_NE(message, "");
460}
461
462TEST_F(ImageDecoderFixtureTest, ImpellerNullColorspace) {
463 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_8888_SkColorType,
464 SkAlphaType::kPremul_SkAlphaType);
465 SkBitmap bitmap;
466 bitmap.allocPixels(info, 10 * 4);
467 auto data = SkData::MakeWithoutCopy(bitmap.getPixels(), 10 * 10 * 4);
468 auto image = SkImages::RasterFromBitmap(bitmap);
469 ASSERT_TRUE(image != nullptr);
470 EXPECT_EQ(SkISize::Make(10, 10), image->dimensions());
471 EXPECT_EQ(nullptr, image->colorSpace());
472
473 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
474 std::move(data), ImageDescriptor::CreateImageInfo(image->imageInfo()),
475 10 * 4);
476
477#if IMPELLER_SUPPORTS_RENDERING
478 std::shared_ptr<impeller::Capabilities> capabilities =
481 .Build();
482 std::shared_ptr<impeller::Allocator> allocator =
483 std::make_shared<impeller::TestImpellerAllocator>();
484 absl::StatusOr<ImageDecoderImpeller::DecompressResult> decompressed =
486 descriptor.get(), {.target_width = 100, .target_height = 100},
487 {100, 100},
488 /*supports_wide_gamut=*/true, capabilities, allocator);
489 ASSERT_TRUE(decompressed.ok());
490 EXPECT_EQ(decompressed->image_info.format,
492#endif // IMPELLER_SUPPORTS_RENDERING
493}
494
495TEST_F(ImageDecoderFixtureTest, ImpellerPixelConversion32F) {
496 auto info = SkImageInfo::Make(10, 10, SkColorType::kRGBA_F32_SkColorType,
497 SkAlphaType::kUnpremul_SkAlphaType);
498 SkBitmap bitmap;
499 bitmap.allocPixels(info, 10 * 16);
500 auto data = SkData::MakeWithoutCopy(bitmap.getPixels(), 10 * 10 * 16);
501 auto image = SkImages::RasterFromBitmap(bitmap);
502
503 ASSERT_TRUE(image != nullptr);
504 EXPECT_EQ(SkISize::Make(10, 10), image->dimensions());
505 EXPECT_EQ(nullptr, image->colorSpace());
506
507 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
508 std::move(data), ImageDescriptor::CreateImageInfo(image->imageInfo()),
509 10 * 16);
510
511#if IMPELLER_SUPPORTS_RENDERING
512 std::shared_ptr<impeller::Capabilities> capabilities =
515 .Build();
516 std::shared_ptr<impeller::Allocator> allocator =
517 std::make_shared<impeller::TestImpellerAllocator>();
518 absl::StatusOr<ImageDecoderImpeller::DecompressResult> decompressed =
520 descriptor.get(), {.target_width = 100, .target_height = 100},
521 {100, 100},
522 /*supports_wide_gamut=*/true, capabilities, allocator);
523
524 ASSERT_TRUE(decompressed.ok());
525 EXPECT_EQ(decompressed->image_info.format,
527#endif // IMPELLER_SUPPORTS_RENDERING
528}
529
530TEST_F(ImageDecoderFixtureTest, ImpellerWideGamutDisplayP3Opaque) {
531 auto data = flutter::testing::OpenFixtureAsSkData("DisplayP3Logo.jpg");
532 auto image = SkCodecs::DeferredImage(SkJpegDecoder::Decode(data, nullptr));
533 ASSERT_TRUE(image != nullptr);
534 ASSERT_EQ(SkISize::Make(100, 100), image->dimensions());
535
536 ImageGeneratorRegistry registry;
537 std::shared_ptr<ImageGenerator> generator =
539 ASSERT_TRUE(generator);
540
541 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
542 std::move(generator));
543
544#if IMPELLER_SUPPORTS_RENDERING
545 std::shared_ptr<impeller::Capabilities> capabilities =
548 .Build();
549 std::shared_ptr<impeller::Allocator> allocator =
550 std::make_shared<impeller::TestImpellerAllocator>();
551 absl::StatusOr<ImageDecoderImpeller::DecompressResult> wide_result =
553 descriptor.get(), {.target_width = 100, .target_height = 100},
554 {100, 100},
555 /*supports_wide_gamut=*/true, capabilities, allocator);
556
557 ASSERT_TRUE(wide_result.ok());
558 ASSERT_EQ(wide_result->image_info.format,
560
561 const uint32_t* pixel_ptr = reinterpret_cast<const uint32_t*>(
562 wide_result->device_buffer->OnGetContents());
563 bool found_deep_red = false;
564 for (int i = 0; i < wide_result->image_info.size.width *
565 wide_result->image_info.size.height;
566 ++i) {
567 uint32_t pixel = *pixel_ptr++;
568 float blue = DecodeBGR10((pixel >> 0) & 0x3ff);
569 float green = DecodeBGR10((pixel >> 10) & 0x3ff);
570 float red = DecodeBGR10((pixel >> 20) & 0x3ff);
571 if (fabsf(red - 1.0931f) < 0.01f && fabsf(green - -0.2268f) < 0.01f &&
572 fabsf(blue - -0.1501f) < 0.01f) {
573 found_deep_red = true;
574 break;
575 }
576 }
577 ASSERT_TRUE(found_deep_red);
578
579 absl::StatusOr<ImageDecoderImpeller::DecompressResult> narrow_result =
581 descriptor.get(), {.target_width = 100, .target_height = 100},
582 {100, 100},
583 /*supports_wide_gamut=*/false, capabilities, allocator);
584
585 ASSERT_TRUE(narrow_result.ok());
586 ASSERT_EQ(narrow_result->image_info.format,
588#endif // IMPELLER_SUPPORTS_RENDERING
589}
590
591TEST_F(ImageDecoderFixtureTest, ImpellerNonWideGamut) {
592 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
593 auto image = SkCodecs::DeferredImage(SkJpegDecoder::Decode(data, nullptr));
594 ASSERT_TRUE(image != nullptr);
595 ASSERT_EQ(SkISize::Make(600, 200), image->dimensions());
596
597 ImageGeneratorRegistry registry;
598 std::shared_ptr<ImageGenerator> generator =
600 ASSERT_TRUE(generator);
601
602 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
603 std::move(generator));
604
605#if IMPELLER_SUPPORTS_RENDERING
606 std::shared_ptr<impeller::Capabilities> capabilities =
609 .Build();
610 std::shared_ptr<impeller::Allocator> allocator =
611 std::make_shared<impeller::TestImpellerAllocator>();
612 absl::StatusOr<ImageDecoderImpeller::DecompressResult> result =
614 descriptor.get(), {.target_width = 600, .target_height = 200},
615 {600, 200},
616 /*supports_wide_gamut=*/true, capabilities, allocator);
617
618 ASSERT_TRUE(result.ok());
619 ASSERT_EQ(result->image_info.format,
621#endif // IMPELLER_SUPPORTS_RENDERING
622}
623
624TEST_F(ImageDecoderFixtureTest, ExifDataIsRespectedOnDecode) {
626 TaskRunners runners(GetCurrentTestName(), // label
627 CreateNewThread("platform"), // platform
628 CreateNewThread("raster"), // raster
629 CreateNewThread("ui"), // ui
630 CreateNewThread("io") // io
631 );
632
634
635 std::unique_ptr<IOManager> io_manager;
636
637 auto release_io_manager = [&]() {
638 io_manager.reset();
639 latch.Signal();
640 };
641
642 SkISize decoded_size = SkISize::MakeEmpty();
643 auto decode_image = [&]() {
644 Settings settings;
645 std::unique_ptr<ImageDecoder> image_decoder = ImageDecoder::Make(
646 settings, runners, loop->GetTaskRunner(),
647 io_manager->GetWeakIOManager(), std::make_shared<fml::SyncSwitch>());
648
649 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
650
651 ASSERT_TRUE(data);
652 ASSERT_GE(data->size(), 0u);
653
654 ImageGeneratorRegistry registry;
655 std::shared_ptr<ImageGenerator> generator =
657 ASSERT_TRUE(generator);
658
659 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
660 std::move(data), std::move(generator));
661
662 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
663 const std::string& decode_error) {
664 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
665
666 auto skia_image = image ? image->asSkiaImage() : nullptr;
667 ASSERT_TRUE(skia_image && skia_image->skia_image());
668 decoded_size = skia_image->skia_image()->dimensions();
669
670 runners.GetIOTaskRunner()->PostTask(release_io_manager);
671 };
672 image_decoder->Decode(
673 descriptor,
674 {.target_width = static_cast<uint32_t>(descriptor->width()),
675 .target_height = static_cast<uint32_t>(descriptor->height())},
676 callback);
677 };
678
679 auto set_up_io_manager_and_decode = [&]() {
680 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
681 runners.GetUITaskRunner()->PostTask(decode_image);
682 };
683
684 runners.GetIOTaskRunner()->PostTask(set_up_io_manager_and_decode);
685
686 latch.Wait();
687
688 ASSERT_EQ(decoded_size.width(), 600);
689 ASSERT_EQ(decoded_size.height(), 200);
690}
691
692TEST_F(ImageDecoderFixtureTest, CanDecodeWithoutAGPUContext) {
694 TaskRunners runners(GetCurrentTestName(), // label
695 CreateNewThread("platform"), // platform
696 CreateNewThread("raster"), // raster
697 CreateNewThread("ui"), // ui
698 CreateNewThread("io") // io
699 );
700
702
703 std::unique_ptr<IOManager> io_manager;
704
705 auto release_io_manager = [&]() {
706 io_manager.reset();
707 latch.Signal();
708 };
709
710 auto decode_image = [&]() {
711 Settings settings;
712 std::unique_ptr<ImageDecoder> image_decoder = ImageDecoder::Make(
713 settings, runners, loop->GetTaskRunner(),
714 io_manager->GetWeakIOManager(), std::make_shared<fml::SyncSwitch>());
715
716 auto data = flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg");
717
718 ASSERT_TRUE(data);
719 ASSERT_GE(data->size(), 0u);
720
721 ImageGeneratorRegistry registry;
722 std::shared_ptr<ImageGenerator> generator =
724 ASSERT_TRUE(generator);
725
726 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
727 std::move(data), std::move(generator));
728
729 ImageDecoder::ImageResult callback = [&](const sk_sp<DlImage>& image,
730 const std::string& decode_error) {
731 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
732
733 auto skia_image = image ? image->asSkiaImage() : nullptr;
734 ASSERT_TRUE(skia_image && skia_image->skia_image());
735
736 runners.GetIOTaskRunner()->PostTask(release_io_manager);
737 };
738 image_decoder->Decode(
739 descriptor,
740 {.target_width = static_cast<uint32_t>(descriptor->width()),
741 .target_height = static_cast<uint32_t>(descriptor->height())},
742 callback);
743 };
744
745 auto set_up_io_manager_and_decode = [&]() {
746 io_manager =
747 std::make_unique<TestIOManager>(runners.GetIOTaskRunner(), false);
748 runners.GetUITaskRunner()->PostTask(decode_image);
749 };
750
751 runners.GetIOTaskRunner()->PostTask(set_up_io_manager_and_decode);
752
753 latch.Wait();
754}
755
756TEST_F(ImageDecoderFixtureTest, CanDecodeWithResizes) {
757 const auto image_dimensions =
758 SkJpegDecoder::Decode(
759 flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg"),
760 nullptr)
761 ->dimensions();
762
763 ASSERT_FALSE(image_dimensions.isEmpty());
764
765 ASSERT_NE(image_dimensions.width(), image_dimensions.height());
766
768 TaskRunners runners(GetCurrentTestName(), // label
769 CreateNewThread("platform"), // platform
770 CreateNewThread("raster"), // raster
771 CreateNewThread("ui"), // ui
772 CreateNewThread("io") // io
773 );
774
776 std::unique_ptr<IOManager> io_manager;
777 std::unique_ptr<ImageDecoder> image_decoder;
778
779 // Setup the IO manager.
780 PostTaskSync(runners.GetIOTaskRunner(), [&]() {
781 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
782 });
783
784 // Setup the image decoder.
785 PostTaskSync(runners.GetUITaskRunner(), [&]() {
786 Settings settings;
787 image_decoder = ImageDecoder::Make(settings, runners, loop->GetTaskRunner(),
788 io_manager->GetWeakIOManager(),
789 std::make_shared<fml::SyncSwitch>());
790 });
791
792 // Setup a generic decoding utility that gives us the final decoded size.
793 auto decoded_size = [&](uint32_t target_width,
794 uint32_t target_height) -> SkISize {
795 SkISize final_size = SkISize::MakeEmpty();
796 runners.GetUITaskRunner()->PostTask([&]() {
797 auto data = flutter::testing::OpenFixtureAsSkData("DashInNooglerHat.jpg");
798
799 ASSERT_TRUE(data);
800 ASSERT_GE(data->size(), 0u);
801
802 ImageGeneratorRegistry registry;
803 std::shared_ptr<ImageGenerator> generator =
805 ASSERT_TRUE(generator);
806
807 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(
808 std::move(data), std::move(generator));
809
811 [&](const sk_sp<DlImage>& image, const std::string& decode_error) {
812 ASSERT_TRUE(runners.GetUITaskRunner()->RunsTasksOnCurrentThread());
813 auto skia_image = image ? image->asSkiaImage() : nullptr;
814 ASSERT_TRUE(skia_image && skia_image->skia_image());
815 final_size = skia_image->skia_image()->dimensions();
816
817 latch.Signal();
818 };
819 image_decoder->Decode(
820 descriptor,
821 {.target_width = target_width, .target_height = target_height},
822 callback);
823 });
824 latch.Wait();
825 return final_size;
826 };
827
828 ASSERT_EQ(SkISize::Make(3024, 4032), image_dimensions);
829 ASSERT_EQ(decoded_size(3024, 4032), image_dimensions);
830 ASSERT_EQ(decoded_size(100, 100), SkISize::Make(100, 100));
831
832 // Destroy the IO manager
833 PostTaskSync(runners.GetIOTaskRunner(), [&]() { io_manager.reset(); });
834
835 // Destroy the image decoder
836 PostTaskSync(runners.GetUITaskRunner(), [&]() { image_decoder.reset(); });
837}
838
839// Verifies https://skia-review.googlesource.com/c/skia/+/259161 is present in
840// Flutter.
841TEST(ImageDecoderTest,
842 VerifyCodecRepeatCountsForGifAndWebPAreConsistentWithLoopCounts) {
843 auto gif_mapping = flutter::testing::OpenFixtureAsSkData("hello_loop_2.gif");
844 auto webp_mapping =
845 flutter::testing::OpenFixtureAsSkData("hello_loop_2.webp");
846
847 ASSERT_TRUE(gif_mapping);
848 ASSERT_TRUE(webp_mapping);
849
850 ImageGeneratorRegistry registry;
851
852 auto gif_generator = registry.CreateCompatibleGenerator(gif_mapping);
853 auto webp_generator = registry.CreateCompatibleGenerator(webp_mapping);
854
855 ASSERT_TRUE(gif_generator);
856 ASSERT_TRUE(webp_generator);
857
858 // Both fixtures have a loop count of 2.
859 ASSERT_EQ(gif_generator->GetPlayCount(), static_cast<unsigned int>(2));
860 ASSERT_EQ(webp_generator->GetPlayCount(), static_cast<unsigned int>(2));
861}
862
863TEST(ImageDecoderTest, VerifySimpleDecoding) {
864 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
865 auto codec = SkJpegDecoder::Decode(data, nullptr);
866 ASSERT_TRUE(codec != nullptr);
867 auto image = SkCodecs::DeferredImage(std::move(codec));
868 ASSERT_TRUE(image != nullptr);
869 EXPECT_EQ(600, image->width());
870 EXPECT_EQ(200, image->height());
871
872 ImageGeneratorRegistry registry;
873 std::shared_ptr<ImageGenerator> generator =
875 ASSERT_TRUE(generator);
876
877 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
878 std::move(generator));
879 auto compressed_image = ImageDecoderSkia::ImageFromCompressedData(
880 descriptor.get(), 6, 2, fml::tracing::TraceFlow(""));
881 EXPECT_EQ(compressed_image->width(), 6);
882 EXPECT_EQ(compressed_image->height(), 2);
883 EXPECT_EQ(compressed_image->alphaType(), kOpaque_SkAlphaType);
884
885#if IMPELLER_SUPPORTS_RENDERING
886 std::shared_ptr<impeller::Capabilities> capabilities =
889 .Build();
890 std::shared_ptr<impeller::Capabilities> capabilities_no_blit =
893 .Build();
894 // Bitmap sizes reflect the original image size as resizing is done on the
895 // GPU if the src size is smaller than the max texture size.
896 std::shared_ptr<impeller::Allocator> allocator =
897 std::make_shared<impeller::TestImpellerAllocator>();
899 descriptor.get(), {.target_width = 6, .target_height = 2}, {1000, 1000},
900 /*supports_wide_gamut=*/false, capabilities, allocator);
901 ASSERT_TRUE(result_1.ok());
902 EXPECT_EQ(result_1->image_info.size.width, 75);
903 EXPECT_EQ(result_1->image_info.size.height, 25);
904
905 // Bitmap sizes reflect the scaled size if the source size is larger than
906 // max texture size even if destination size isn't max texture size.
908 descriptor.get(), {.target_width = 6, .target_height = 2}, {10, 10},
909 /*supports_wide_gamut=*/false, capabilities, allocator);
910 ASSERT_TRUE(result_2.ok());
911 EXPECT_EQ(result_2->image_info.size.width, 6);
912 EXPECT_EQ(result_2->image_info.size.height, 2);
913
914 // If the destination size is larger than the max texture size the image
915 // is scaled down.
917 descriptor.get(), {.target_width = 60, .target_height = 20}, {10, 10},
918 /*supports_wide_gamut=*/false, capabilities, allocator);
919 ASSERT_TRUE(result_3.ok());
920 EXPECT_EQ(result_3->image_info.size.width, 10);
921 EXPECT_EQ(result_3->image_info.size.height, 10);
922
923 // CPU resize is forced.
925 descriptor.get(), {.target_width = 6, .target_height = 2}, {1000, 1000},
926 /*supports_wide_gamut=*/false, capabilities_no_blit, allocator);
927 ASSERT_TRUE(result_4.ok());
928 EXPECT_EQ(result_4->image_info.size.width, 6);
929 EXPECT_EQ(result_4->image_info.size.height, 2);
930#endif // IMPELLER_SUPPORTS_RENDERING
931}
932
933TEST(ImageDecoderTest, ImagesWithTransparencyArePremulAlpha) {
934 auto data = flutter::testing::OpenFixtureAsSkData("heart_end.png");
935 ASSERT_TRUE(data);
936 ImageGeneratorRegistry registry;
937 std::shared_ptr<ImageGenerator> generator =
939 ASSERT_TRUE(generator);
940
941 auto descriptor = fml::MakeRefCounted<ImageDescriptor>(std::move(data),
942 std::move(generator));
943 auto compressed_image = ImageDecoderSkia::ImageFromCompressedData(
944 descriptor.get(), 250, 250, fml::tracing::TraceFlow(""));
945 ASSERT_TRUE(compressed_image);
946 ASSERT_EQ(compressed_image->width(), 250);
947 ASSERT_EQ(compressed_image->height(), 250);
948 ASSERT_EQ(compressed_image->alphaType(), kPremul_SkAlphaType);
949}
950
951TEST(ImageDecoderTest, VerifySubpixelDecodingPreservesExifOrientation) {
952 auto data = flutter::testing::OpenFixtureAsSkData("Horizontal.jpg");
953
954 ImageGeneratorRegistry registry;
955 std::shared_ptr<ImageGenerator> generator =
957 ASSERT_TRUE(generator);
958 auto descriptor =
959 fml::MakeRefCounted<ImageDescriptor>(data, std::move(generator));
960
961 // If Exif metadata is ignored, the height and width will be swapped because
962 // "Rotate 90 CW" is what is encoded there.
963 ASSERT_EQ(600, descriptor->width());
964 ASSERT_EQ(200, descriptor->height());
965
966 auto image = SkCodecs::DeferredImage(SkJpegDecoder::Decode(data, nullptr));
967 ASSERT_TRUE(image != nullptr);
968 ASSERT_EQ(600, image->width());
969 ASSERT_EQ(200, image->height());
970}
971
973 MultiFrameCodecCanBeCollectedBeforeIOTasksFinish) {
974 // This test verifies that the MultiFrameCodec safely shares state between
975 // tasks on the IO and UI runners, and does not allow unsafe memory access if
976 // the UI object is collected while the IO thread still has pending decode
977 // work. This could happen in a real application if the engine is collected
978 // while a multi-frame image is decoding. To exercise this, the test:
979 // - Starts a Dart VM
980 // - Latches the IO task runner
981 // - Create a MultiFrameCodec for an animated gif pointed to a callback
982 // in the Dart fixture
983 // - Calls getNextFrame on the UI task runner
984 // - Collects the MultiFrameCodec object before unlatching the IO task
985 // runner.
986 // - Unlatches the IO task runner
987 auto settings = CreateSettingsForFixture();
988 auto vm_ref = DartVMRef::Create(settings);
989 auto vm_data = vm_ref.GetVMData();
990
991 auto gif_mapping = flutter::testing::OpenFixtureAsSkData("hello_loop_2.gif");
992
993 ASSERT_TRUE(gif_mapping);
994
995 ImageGeneratorRegistry registry;
996 std::shared_ptr<ImageGenerator> gif_generator =
997 registry.CreateCompatibleGenerator(gif_mapping);
998 ASSERT_TRUE(gif_generator);
999
1000 TaskRunners runners(GetCurrentTestName(), // label
1001 CreateNewThread("platform"), // platform
1002 CreateNewThread("raster"), // raster
1003 CreateNewThread("ui"), // ui
1004 CreateNewThread("io") // io
1005 );
1006
1008 std::unique_ptr<TestIOManager> io_manager;
1009
1010 // Setup the IO manager.
1011 PostTaskSync(runners.GetIOTaskRunner(), [&]() {
1012 io_manager = std::make_unique<TestIOManager>(runners.GetIOTaskRunner());
1013 });
1014
1015 auto isolate = RunDartCodeInIsolate(vm_ref, settings, runners, "main", {},
1017 io_manager->GetWeakIOManager());
1018
1019 // Latch the IO task runner.
1020 runners.GetIOTaskRunner()->PostTask([&]() { io_latch.Wait(); });
1021
1022 PostTaskSync(runners.GetUITaskRunner(), [&]() {
1023 fml::AutoResetWaitableEvent isolate_latch;
1024 fml::RefPtr<MultiFrameCodec> codec;
1025 EXPECT_TRUE(isolate->RunInIsolateScope([&]() -> bool {
1026 Dart_Handle library = Dart_RootLibrary();
1027 if (Dart_IsError(library)) {
1028 isolate_latch.Signal();
1029 return false;
1030 }
1031 Dart_Handle closure =
1032 Dart_GetField(library, Dart_NewStringFromCString("frameCallback"));
1033 if (Dart_IsError(closure) || !Dart_IsClosure(closure)) {
1034 isolate_latch.Signal();
1035 return false;
1036 }
1037
1038 codec = fml::MakeRefCounted<MultiFrameCodec>(std::move(gif_generator));
1039 codec->getNextFrame(closure);
1040 codec = nullptr;
1041 isolate_latch.Signal();
1042 return true;
1043 }));
1044 isolate_latch.Wait();
1045
1046 EXPECT_FALSE(codec);
1047
1048 io_latch.Signal();
1049 });
1050
1051 // Destroy the IO manager
1052 PostTaskSync(runners.GetIOTaskRunner(), [&]() { io_manager.reset(); });
1053}
1054
1056 auto context = std::make_shared<impeller::TestImpellerContext>();
1057 auto allocator = ImpellerAllocator(context->GetResourceAllocator());
1058
1059 EXPECT_FALSE(allocator.allocPixelRef(nullptr));
1060}
1061
1062} // namespace testing
1063} // namespace flutter
1064
1065// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
1066// 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:69
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,...
bool FinishQueue() override
Wait until all previously submitted command buffers are processed and displayed by the GPU.
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.