Skip to content

Image loading is significantly slower for non-sRGB color space images when Impeller is enabled #177805

@b-luk

Description

@b-luk

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 sRGB.jpeg

jpeg-1 2770x4155 Display P3.jpeg

jpeg-2 4000x6000 sRGB.jpeg

jpeg-2 4000x6000 Display P3.jpeg

jpeg-3 519x600 sRGB.jpeg

jpeg-3 519x600 Display P3.jpeg

png-1 4320x4320 sRGB.png

png-1 4320x4320 Display P3.png

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]

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: imagesLoading, displaying, rendering imagesc: performanceRelates to speed or footprint issues (see "perf:" labels)e: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.team-engineOwned by Engine teamtriaged-engineTriaged by Engine team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions