-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Steps to reproduce
With impeller enabled, load and display an image (e.g. Image.asset()) that uses a non-sRGB color space.
Loading images that uses a non-sRGB color space is significantly slower compared to loading an image that uses the sRGB color space.
Metrics
I added debug logging to the engine for the codec_->getPixels call in BuiltinSkiaCodecImageGenerator::GetPixels:
engine/src/flutter/lib/ui/painting/image_generator.cc
@@ -176,7 +178,12 @@ bool BuiltinSkiaCodecImageGenerator::GetPixels(
temp_pixmap = temp_bitmap.pixmap();
}
+ const auto start = std::chrono::steady_clock::now();
SkCodec::Result result = codec_->getPixels(temp_pixmap, &options);
+ const auto end = std::chrono::steady_clock::now();
+ const std::chrono::duration<double, std::milli> diff = end - start;
+ FML_LOG(ERROR) << "getPixels time: " << diff;
if (result != SkCodec::kSuccess) {
FML_DLOG(WARNING) << "codec could not get pixels. "
<< SkCodec::ResultToString(result);I ran the sample app below in profile mode. The sample app shows 8 images in sequence as the FAB is tapped. The images consist of 4 pairs of visually identical images. The first image in each pair uses the sRGB color space, and the second uses the Display P3 color space.
Results are below. The times are example times for one run. Running this test multiple times produces times that vary by a small amount, but the overall timings and trends stay consistent.
Android - Pixel 8 - impeller enabled
| image | sRGB | Display P3 | change |
|---|---|---|---|
| jpeg-1 2770x4155 | 213ms | 1243ms | 5.8x |
| jpeg-2 4000x6000 | 415ms | 2306ms | 5.6x |
| jpeg-3 519x600 | 5.88ms | 61ms | 10.4x |
| png-1 4320x4320 | 51ms | 342ms | 6.7x |
Android - Pixel 8 - impeller disabled
| image | sRGB | Display P3 | change |
|---|---|---|---|
| jpeg-1 2770x4155 | 222ms | 183ms | 0.82x |
| jpeg-2 4000x6000 | 465ms | 538ms | 1.2x |
| jpeg-3 519x600 | 6.81ms | 5.69ms | 0.8x |
| png-1 4320x4320 | 81ms | 86ms | 1.06x |
iOS - iPhone 12 - impeller enabled
| image | sRGB | Display P3 | change |
|---|---|---|---|
| jpeg-1 2770x4155 | 215ms | 294ms | 1.4x |
| jpeg-2 4000x6000 | 437ms | 730ms | 1.7x |
| jpeg-3 519x600 | 12.6ms | 26.2ms | 2.1x |
| png-1 4320x4320 | 90ms | 276ms | 3.1x |
On both Android and iOS with impeller enabled, the Display P3 color space images take many times longer to load than the identical sRGB color space images. On my Android device, the slowdown was more drastic compared to my iOS device.
On Android with impeller disabled, the loading times stay relatively close irrespective of sRGB vs Display P3 color space.
Root cause
I narrowed down the speed difference to code that runs during image decoding to convert from one color space to another. E.g. for jpeg, the slowdown was caused by calls to applyColorXform here.
This conversion is performed for non-sRGB images when impeller is enabled. But it is skipped when impeller is disabled.
The difference is in the ImageDecoder classes for impeller vs skia (impeller disabled).
In ImageDecoderSkia, image decoding happens in ImageDecoderSkia::ImageFromCompressedData when descriptor->image() or descriptor->get_pixels(pixmap) is called. Both of these calls decode the image using the original color space of the input ImageDescriptor.
In ImageDecoderImpeller, image decoding happens in ImageDecoderImpeller::DecompressTexture when descriptor->get_pixels(bitmap->pixmap()) is called. The input pixmap to this call has an image_info based on the input descriptor's image_info, but with a color space explicitly set to SkColorSpace::MakeSRGB() here. This instructs the decoder that the decoded output should use the sRGB color space. If the input compressed image is in a different color space, then during decoding the result must be converted to sRGB.
This difference means that with impeller disabled, decoding images does not perform any color space conversion. But with impeller enabled, decoding images will convert any non-sRGB color space to be sRGB. This color space conversion during decoding is performed on the CPU, which may be slow, and it causes the slow image decoding seen in the metrics above.
The skia image decoder does not need to convert to SRGB because it decodes the image into an SkImage, which includes an SkImageInfo describing the image's color space. When the image is eventually rendered by the Skia renderer, it renders this SkImage object taking into account its color space. I assume this happens on the GPU, where it is much faster than doing a color space conversion on the CPU.
The impeller image decoder, on the other hand, decodes images into impeller::Texture objects, which do not keep track of the image's color space. I assume the impeller renderer renders these textures with the assumption that the color space is sRGB. So with impeller, the image decoder must perform a color space conversion for any non-sRBG images when decoding an image into a texture.
A potential fix for this would be to make impeller::Texture keep track of the image's color space, rather than assuming everything is sRGB. This way, the decoder does not need to perform a color space conversion when decoding into a texture. Then, when the texture is rendered, the renderer should take into account the texture's color space and use the GPU to perform any required color space conversions.
Code sample
The sample app shows 8 images in sequence as the FAB is tapped. The images consist of 4 pairs of visually identical images. The first image in each pair uses the sRGB color space, and the second uses the Display P3 color space.
Code sample
import 'package:flutter/material.dart';
const assets = [
'',
'assets/jpeg-1 2770x4155 sRGB.jpeg',
'assets/jpeg-1 2770x4155 Display P3.jpeg',
'assets/jpeg-2 4000x6000 sRGB.jpeg',
'assets/jpeg-2 4000x6000 Display P3.jpeg',
'assets/jpeg-3 519x600 sRGB.jpeg',
'assets/jpeg-3 519x600 Display P3.jpeg',
'assets/png-1 4320x4320 sRGB.png',
'assets/png-1 4320x4320 Display P3.png',
];
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
_counter %= assets.length;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: .center,
children: _buildContents(_counter),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
List<Widget> _buildContents(int i) {
if (i == 0) return [Text('Press button for image')];
return [
Image.asset(assets[i], width: 300, height: 300),
Text('Image $i of ${assets.length - 1}'),
Text(assets[i]),
];
}
Uses the following image assets. Each must be renamed to the expected file name shown in the sample app.
Images
jpeg-1 2770x4155 Display P3.jpeg

jpeg-2 4000x6000 Display P3.jpeg

Performance profiling on master channel
- The issue still persists on the master channel
Timeline Traces
Timeline Traces JSON
[Paste the Timeline Traces here]Video demonstration
Videos are recorded on a Pixel 8 in profile mode.
Video demonstration
Impeller enabled
The Display P3 images take longer to load than the sRGB images.
impeller.enabled.mp4
Impeller disabled
The Display P3 images take about the same time load as the sRGB images.
impeller.disabled.mp4
What target platforms are you seeing this bug on?
Android, iOS
OS/Browser name and version | Device information
Android: Pixel 8, Android 16
iOS: iPhone 12, iOS 17.1.1
Does the problem occur on emulator/simulator as well as on physical devices?
Unknown
Is the problem only reproducible with Impeller?
Yes
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[Paste your output here]




