Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add workaround for AOSP bug with decoding single channel hardware gai… #5357

Merged
merged 1 commit into from
Feb 2, 2024

Conversation

falhassen
Copy link
Collaborator

…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks single channel gainmap rendering using hardware bitmaps, which is a common use case on devices that have Ultra HDR as the default capture format.

The workaround is as follows:

  1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
  2. Memoize the result
  3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as devices are updated with this fix, this flow will no longer be exercised.

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 24, 2024

I can try adding emulator tests, but I wanted to check on overall feedback with this approach before adding tests I might have to erase depending on reviewer feedback.

I will need to spend some time to see if the bug can be reproduced in emulators. I'm not sure if hardware bitmap flows are testable using emulators.

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 24, 2024

I added a flag guard too, so this should not break anyone who doesn't opt-in into the bug fix.

@sjudd
Copy link
Collaborator

sjudd commented Jan 26, 2024

What if you turn off hardware Bitmaps for U until the fix goes out?

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 26, 2024 via email

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 26, 2024 via email

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 26, 2024 via email

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 26, 2024 via email

softwareBitmap =
BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options);
if (softwareBitmap == null) {
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block of code is repeated a bunch, I think you could do something like this instead:

    private static Bitmap safeDecodeHardwareBitmapWithGainmap(byte[] bytes, Options options) {
        try {
            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
            Bitmap softwareBitmap =
                    BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options);
            return safeDecodeBitmapWithGainmapCommon(softwareBitmap);
        } finally {
            options.inPreferredConfig = Config.HARDWARE;
        }
    }

    private static Bitmap safeDecodeBitmapWithGainmapCommon(Bitmap softwareBitmap) {
        if (softwareBitmap == null) {
            return null;
        }
        try {
            Gainmap gainmap = softwareBitmap.getGainmap();
            if (gainmap != null) {
                Bitmap gainmapContents = gainmap.getGainmapContents();
                if (gainmapContents.getConfig() == Bitmap.Config.ALPHA_8) {
                    softwareBitmap.setGainmap(
                            GainmapCopier.convertSingleChannelGainmapToTripleChannelGainmap(gainmap));
                }
            }
            return softwareBitmap.copy(Bitmap.Config.HARDWARE, /* isMutable= */ false);
        } finally {
            softwareBitmap.recycle();
        }
    }

Copy link
Collaborator Author

@falhassen falhassen Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip. Done.

@falhassen
Copy link
Collaborator Author

falhassen commented Jan 26, 2024

I added emulator tests to DownsamplerEmulatorTest. Unfortunately, my emulated device doesn't seem to reproduce this framework issue, but at least they can guard against regressions. Certainly, the emulated device can reproduce the non-default exif orientation bug when I disable the fix.

* Fixes decoding of hardware gainmaps from Ultra HDR images on Android U.
*
* <p>Without this flag on, gainmaps may be dropped when decoding Ultra HDR on Android U devices
* using skiagl for hwui..
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can say more words, what's the background and why would someone not want to turn this on? Should/will it be default?

Maybe just link to the source where this is described in detail?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

@Nullable
@Override
public Bitmap decodeBitmap(Options options) {
if (enableHardwareGainmapFixOnU
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this repeated in all places we might decode a bitmap, can you create a GlideBitmapFactory wrapper where we can do this logic once? You kind of have it in AndroidUHardwareBitmapFactoryDecoder. Or is your plan to merge this when you are cleaning up the experiment?

Otherwise it seems easy to accidentally introduce another unguarded usage of BitmapDecoding and accidentally break something on Android U, ideally we'd discourage direct usage of BitmapFactory too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return false;
}
Boolean requiresGainmapDecodeWorkaround = REQUIRES_GAIN_MAP_FIX.get();
if (requiresGainmapDecodeWorkaround == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it ever make sense to start this computation in multiple threads? I don't think it does, so this should probably be synchronzied and use double-checked locking?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. This is a bug. I fixed it (and added a @GuardedBy)

* @param softwareBitmap The bitmap to be decoded. Must not be a hardware bitmap..
*/
public static Bitmap safeDecodeBitmapWithGainmap(Bitmap softwareBitmap) {
if (softwareBitmap == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't valid input to this method? If it is it should be @Nullable

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Removed.

if (softwareBitmap != null) {
softwareBitmap.recycle();
}
options.inPreferredConfig = Config.HARDWARE;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It took me a second to convince myself that you're just resetting it to what it was before, but I think it would be easier to reason about this method if you add an assertion at the start of it that the options are indeed set to HARDWARE.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

falhassen added a commit to falhassen/glide that referenced this pull request Jan 30, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
falhassen added a commit to falhassen/glide that referenced this pull request Jan 30, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
falhassen added a commit to falhassen/glide that referenced this pull request Jan 30, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
@falhassen
Copy link
Collaborator Author

I added emulator tests to DownsamplerEmulatorTest. Unfortunately, my emulated device doesn't seem to reproduce this framework issue, but at least they can guard against regressions. Certainly, the emulated device can reproduce the non-default exif orientation bug when I disable the fix.

Actually, I found a bug in my commit where I configured the gainmap to use ARGB_8888. To reproduce the bug, I need to configure the gainmap to use ALPHA_8. Once I do that, the emulator test will fail without the workaround.

falhassen added a commit to falhassen/glide that referenced this pull request Jan 30, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
falhassen added a commit to falhassen/glide that referenced this pull request Jan 31, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
falhassen added a commit to falhassen/glide that referenced this pull request Jan 31, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
falhassen added a commit to falhassen/glide that referenced this pull request Jan 31, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
* <p>New usages of {@link BitmapFactory} APIs within Glide should be added here rather than called
* directly.
*/
final class GlideBitmapFactoryWrapper {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: drop the Wrapper? it's cleaner

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private static Bitmap safeDecodeHardwareBitmapWithGainmap(
InputStream inputStream, Options options) {
Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE);
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're setting options twice (once here and once inside the try). It should be the last statement before the try {

Applies to this and all the methods below

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops thanks for catching this.

*
* @param options which will be used to decode the gainmap.
*/
static synchronized boolean needsGainmapDecodeWorkaround(Options options) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can be a bit more efficient with the locking, some of the preamble here doesn't have to be synchronized, and they don't need a shared lock until REQUIRES_GAIN_MAP_FIX is found to be null?

Not sure if it matters that much, but we do know these methods are invoked from 4 threads concurrently.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Sorry this had violated the Effective Java rule of avoid excessive synchronization.

I switched over to using GlideSupplier, which is the Glide way of memoizing expensive compuatiohns.

falhassen added a commit to falhassen/glide that referenced this pull request Jan 31, 2024
…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
@falhassen
Copy link
Collaborator Author

Sorry, I think there is a bug in my last push. Will update once I fix...

}

@Nullable
@Override
public Bitmap decodeBitmap(Options options) {
return BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options);
return enableHardwareGainmapFixOnU
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here and below is that once enableHardwareGainmapFixOnU is permanently shipped, we remove enableHardwareGainmapFixOnU and only call into GlideBitmapFactory, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. We will clean it up as we would any Glide flag.

…nmaps using BitmapFactory.

Android U devices that use skiagl for hwui renderering (currently the vast majority) have a bug
where uplading single channel bitmaps to hardware bitmaps fails. This bug essentially breaks
single channel gainmap rendering using hardware bitmaps, which is a common use case on devices
that have Ultra HDR as the default capture format. See bumptech#5357
for more details.

The workaround is as follows:
1. Detect the presence of the bug by attempting to copy a 1x1 single channel bitmap to hardware.
2. Memoize the result
3. For BitmapFactory.decode* calls, if the input bitmap has a gainmap, copy it to a three-channel
gainmap. This step seems wasteful, but in practice, the Android OS already does some copying when
creating hardware bitmaps. Also, more importantly, there is an AOSP fix for this bug, and as
devices are updated with this fix, this flow will no longer be exercised.
@falhassen
Copy link
Collaborator Author

I fixed the issue. The bitmap.copy operation was setting all alpha values to zero. I copied the gainmap with a filter forcing the 100% opacity (which should be working-as-intended, since ALPHA_8 images only have a single channel).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants