[Android] Support HDR output#101977
Conversation
3746efd to
22a8b3f
Compare
4281d5a to
066b7fc
Compare
066b7fc to
442fe8c
Compare
godotengine/godot-proposals#7878 might help with this, but with a performance cost. |
e1a4e4c to
a15a60f
Compare
|
Rebased on the latest HDR code |
7adc97e to
2a694ab
Compare
|
Added back in support for HDR10 in order to get HDR working on my Google Pixel devices. It seems to be working now. Luminance does not currently adjust when the screen brightness is changed, so that still needs to be added. There is also a bug where HDR must be turned off, then back on, in order to work. |
2a694ab to
65d6265
Compare
15fed96 to
f4ba0b4
Compare
|
Added support for adjusting dynamically to HDR/SDR ratio changes (for devices that support that), falling back to checking every 500 ms for devices that do not. I think this is ready to be reviewed now. |
e41d150 to
93a6af5
Compare
|
@Calinou I believe the contrast issues may be fixed now, could you try again when you get a chance? Thanks. |
| @@ -456,6 +457,42 @@ void DisplayServerAndroid::_window_callback(const Callable &p_callable, bool p_d | |||
| } | |||
| } | |||
|
|
|||
| void DisplayServerAndroid::_update_hdr_output(const AndroidHdrCapabilities &p_hdr_capabilities) { | |||
There was a problem hiding this comment.
This should include a p_window_id parameter that defaults to MAIN_WINDOW_ID.
I'm working on adding multi-window support to Android, so I'd imagine we want to update the hdr output per window.
| ERR_FAIL_COND_MSG(p_enable && (rendering_device && rendering_device->has_feature(RenderingDevice::Features::SUPPORTS_HDR_OUTPUT)) == false, "HDR output is not supported by the rendering device."); | ||
| #endif | ||
|
|
||
| hdr_output_requested = p_enable; |
There was a problem hiding this comment.
Per my comment above, this should be a Map<WindowID, bool>
| return; | ||
| } | ||
|
|
||
| hdr_output_reference_luminance = p_reference_luminance; |
There was a problem hiding this comment.
Per my comment above, this should be a Map<WindowID, float>
| return; | ||
| } | ||
|
|
||
| hdr_output_max_luminance = p_max_luminance; |
There was a problem hiding this comment.
Per my comment above, this should be a Map<WindowID, float>
I've tested it again at various brightness levels on the device, and the issue still appears to be here. It seems the black level is raised over the entire scene in both 2D and 3D (even pure black in the 2D HDR tester seems to be more like a shade of dark gray). Is it possible to take HDR screenshots on an Android device so I can (try to) send an example of what it looks like? If not, I can try to take photos with another device with fixed settings. |
93a6af5 to
89e4633
Compare
89e4633 to
d045865
Compare
|
Thanks for your work on this, DarkKilauea! I've finally had a chance to try this PR with a Google Pixel 9a that I picked up a few weeks back. Here are some notes:
(Notice that SDR parts of the image are a similar brightness, but parts of the image that should be up to 8 times brighter than SDR reference white are not that bright.) I also have an EXR HDR version of this photo. You can turn up the exposure on this HDR image to see that SDR parts of the image are approximately the same brightness: https://drive.google.com/file/d/15HD_B3B3iOZ7dxFizZyEjtntWsBEzzQB/view?usp=sharing Additionally, the max brightness magenta colour on the right side of the colour sweep looks especially dark compared to the other colours in the colour sweep on Android HDR. This does not happen when running in SDR mode on Android. My suspicion is that this is related to some sort of tonemapping that is being applied to the image. I have not yet tested how this Pixel 9a behaves when presenting through I've tried opening the Sponza scene as described by Calinou... But I am not able to reproduce any contrast issues. The only issues with the Sponza scene are those I described above. It's possible this contrast issue is specific to how the Samsung Galaxy S25 Ultra handles tonemapping for HDR10, but I couldn't say for sure. The next step in understanding these issues will likely to be trying a prototype of presenting via |
Nice tests! I also have a Pixel 9a, was this done with the Enhanced HDR Brightness setting enabled or disabled? That may be a reason for strange reported values, although since it was enabled by default for me, it would be best to handle both with it off and with it on at various intensities. There is also an Extra Dim setting (and Night Light and Color Correction) which might affect it too. Color Correction seems to be passed to the application somehow, because some apps restart after it's toggled, but we may not need to handle it in any special way. |
15135b7 to
729e796
Compare
729e796 to
4ef41eb
Compare
Strangely, my Google Pixel 9a does not have an "Enhanced HDR Brightness" mode; it only has an "Adaptive brightness" mode. Those photos and tests were done with Adaptive brightness, Night Light, and Extra dim turned off and Colors set to Natural.
This seems to have introduced a new issue where |
4ef41eb to
b1d089c
Compare
| Maximum value for HDR output in multiples of the reference luminance. If [code]0.0[/code], the screen's maximum brightness will be used. | ||
| [b]Note:[/b] This setting is only implemented on Android. On other platforms, the display's maximum brightness is always used for HDR output, and this setting is ignored. | ||
| </member> | ||
| <member name="display/window/hdr/max_output_value.android" type="float" setter="" getter="" default="4.0"> |
There was a problem hiding this comment.
Is this needed if the above setting is only implemented on Android?
There was a problem hiding this comment.
I strongly feel that for 4.7 the Android Android "desired HDR headroom" should be hardcoded to something like 8.0 (this is the highest that my Google Pixel is able to go and also the highest my iPhone 13 Pro is able to go, so this makes sense as a good number to hardcode) and not exposed to the user in any way.
Godot 4.8 will introduce a new Window.output_linear_value_limit property which will be available on all platforms. For most platforms it will probably be best to default to 128.0 (for reasons I will describe in my upcoming proposal) and for Android and iOS it will probably be best to default to 8.0.
In 4.8, this new property may be linked directly to the Android "desired HDR headroom" and equivalent on iOS.
I am focused on finishing 4.7 for now, but once that's wrapped up, I will start work on the user-facing Window.output_linear_value_limit property and all of the related platform-specific features.
|
We chatted about how to address the issues described in the comments here and here in the last rendering meeting. There are a few things left to look into. From my understanding, these two issues are caused by GPU drivers (or some code that is outside of our control) performing some sort of tonemapping or gamut mapping to bring an HDR10 colour space (a.k.a. I really don't know what is possible because I haven't worked much with Vulkan or Android APIs, but from what I've read and discussed with others, I expect the solution is to do something like this...
If this approach doesn't work, the HDR mode will appear too dark. So it will be pretty easy to test. Regarding step 3, this where it makes sense to add } else if (data.target_color_space == COLOR_SPACE_REC709_NONLINEAR_SRGB) {
// Negative values and values above 1.0 will be clipped by the target,
// so no need to clip them here.
if (data.source_is_srgb == false) {
// Android HDR scaling (make sure the reference_multiplier equals 1.0 when not using Android HDR)
color.rgb /= data.reference_multiplier;
// linear -> sRGB conversion
color.rgb = linear_to_srgb(color.rgb);
// Even if debanding was applied earlier in the rendering process, it must
// be reapplied after the linear_to_srgb floating point operations.
// When the linear_to_srgb operation was not performed, the source is
// already an 8-bit format and debanding cannot be effective. In this
// case, GPU driver rounding error can add noise so debanding should be
// skipped entirely.
if (data.use_debanding) {
color.rgb += screen_space_dither(gl_FragCoord.xy);
}
}
}For the final PR, if this approach works, we need to add a new 10-bit Rough sketch of what a blit could look like with a new colour space and debanding updated// From https://alex.vlachos.com/graphics/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
// and https://www.shadertoy.com/view/MslGR8 (5th one starting from the bottom)
// NOTE: `frag_coord` is in pixels (i.e. not normalized UV).
// This dithering must be applied after encoding changes (linear/nonlinear) have been applied
// as the final step before quantization from floating point to integer values.
vec3 screen_space_dither(vec2 frag_coord, float bit_alignment_diviser) {
// Iestyn's RGB dither (7 asm instructions) from Portal 2 X360, slightly modified for VR.
// Removed the time component to avoid passing time into this shader.
vec3 dither = vec3(dot(vec2(171.0, 231.0), frag_coord));
dither.rgb = fract(dither.rgb / vec3(103.0, 71.0, 97.0));
// Subtract 0.5 to avoid slightly brightening the whole viewport.
// Use a dither strength of 100% rather than the 37.5% suggested by the original source.
return (dither.rgb - 0.5) / bit_alignment_diviser;
}
...
// Colorspace conversion for final blit.
if (data.target_color_space == COLOR_SPACE_REC709_LINEAR) {
(unchanged)
} else if (data.target_color_space == COLOR_SPACE_REC709_NONLINEAR_SRGB) {
// Negative values and values above 1.0 will be clipped by the target,
// so no need to clip them here.
if (data.source_is_srgb == false) {
// linear -> sRGB conversion
color.rgb = linear_to_srgb(color.rgb);
// Even if debanding was applied earlier in the rendering process, it must
// be reapplied after the linear_to_srgb floating point operations.
// When the linear_to_srgb operation was not performed, the source is
// already an 8-bit format and debanding cannot be effective. In this
// case, GPU driver rounding error can add noise so debanding should be
// skipped entirely.
if (data.use_debanding) {
// Divide by 255 to align to 8-bit quantization.
color.rgb += screen_space_dither(gl_FragCoord.xy, 255.0); // <-- New parameter
}
} else if (data.target_color_space == COLOR_SPACE_REC709_NONLINEAR_SRGB_HDR) { // <-- New COLOR_SPACE
// Negative values and values above output_max_value will be clipped by the target,
// so no need to clip them here.
// Android HDR scaling
color.rgb /= data.reference_multiplier;
// linear -> sRGB conversion
color.rgb = linear_to_srgb(color.rgb);
if (data.use_debanding) {
// Divide by 1023 to align to 10-bit quantization.
color.rgb += screen_space_dither_10_bit(gl_FragCoord.xy, 1023.0);
}
} |


Based on #94496
Implements godotengine/godot-proposals#10817 for Android
Testing/Sample project: https://github.com/DarkKilauea/godot-hdr-output
Adds support for rendering HDR output on Android devices, should only be merged after #94496. See the last commit for android specific details.
HDR 10 support is added back in via this PR due to my Android devices requiring it in order to show an HDR output. Looking at https://developer.android.com/reference/android/view/Display.HdrCapabilities it doesn't look like there is any support for HDR output via a linear Rec 709 color space.
Known issues:
Forward Mobile seems to squash the dynamic range too much to really show the "HDR effect".SDR white level is a guess due to the lack of APIs to get the actual value. Works well on the Pixel phones I have.Not sure ifWindow.setColorModeis actually necessary to get HDR output to show.