1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.graphics; 18 19 import android.annotation.FlaggedApi; 20 import android.annotation.FloatRange; 21 import android.annotation.NonNull; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 25 import com.android.graphics.hwui.flags.Flags; 26 27 import libcore.util.NativeAllocationRegistry; 28 29 /** 30 * Gainmap represents a mechanism for augmenting an SDR image to produce an HDR one with variable 31 * display adjustment capability. It is a combination of a set of metadata describing how to apply 32 * the gainmap, as well as either a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3 33 * (such as {@link android.graphics.Bitmap.Config#ARGB_8888} with the alpha channel ignored) 34 * channel Bitmap that represents the gainmap data itself. 35 * <p> 36 * When rendering to an {@link android.content.pm.ActivityInfo#COLOR_MODE_HDR} activity, the 37 * hardware accelerated {@link Canvas} will automatically apply the gainmap when sufficient 38 * HDR headroom is available. 39 * 40 * <h3>Gainmap Structure</h3> 41 * 42 * The logical whole of a gainmap'd image consists of a base Bitmap that represents the original 43 * image as would be displayed without gainmap support in addition to a gainmap with a second 44 * enhancement image. In the case of a JPEG, the base image would be the typical 8-bit SDR image 45 * that the format is commonly associated with. The gainmap image is embedded alongside the base 46 * image, often at a lower resolution (such as 1/4th), along with some metadata to describe 47 * how to apply the gainmap. The gainmap image itself is then a greyscale image representing 48 * the transformation to apply onto the base image to reconstruct an HDR rendition of it. 49 * <p> 50 * As such these "gainmap images" consist of 3 parts - a base {@link Bitmap} with a 51 * {@link Bitmap#getGainmap()} that returns an instance of this class which in turn contains 52 * the enhancement layer represented as another Bitmap, accessible via {@link #getGainmapContents()} 53 * 54 * <h3>Applying a gainmap manually</h3> 55 * 56 * When doing custom rendering such as to an OpenGL ES or Vulkan context, the gainmap is not 57 * automatically applied. In such situations, the following steps are appropriate to render the 58 * gainmap in combination with the base image. 59 * <p> 60 * Suppose our display has HDR to SDR ratio of H, and we wish to display an image with gainmap on 61 * this display. Let B be the pixel value from the base image in a color space that has the 62 * primaries of the base image and a linear transfer function. Let G be the pixel value from the 63 * gainmap. Let D be the output pixel in the same color space as B. The value of D is computed 64 * as follows: 65 * <p> 66 * First, let W be a weight parameter determining how much the gainmap will be applied. 67 * <pre class="prettyprint"> 68 * W = clamp((log(H) - log(minDisplayRatioForHdrTransition)) / 69 * (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1)</pre> 70 * 71 * Next, let L be the gainmap value in log space. We compute this from the value G that was 72 * sampled from the texture as follows: 73 * <pre class="prettyprint"> 74 * L = mix(log(ratioMin), log(ratioMax), pow(G, gamma))</pre> 75 * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then 76 * compute: 77 * <pre class="prettyprint"> 78 * D = (B + epsilonSdr) * exp(L * W) - epsilonHdr</pre> 79 * <p> 80 * In the above math, log() is a natural logarithm and exp() is natural exponentiation. The base 81 * for these functions cancels out and does not affect the result, so other bases may be used 82 * if preferred. 83 */ 84 public final class Gainmap implements Parcelable { 85 86 // Use a Holder to allow static initialization of Gainmap in the boot image. 87 private static class NoImagePreloadHolder { 88 public static final NativeAllocationRegistry sRegistry = 89 NativeAllocationRegistry.createMalloced( 90 Gainmap.class.getClassLoader(), nGetFinalizer()); 91 } 92 93 final long mNativePtr; 94 private Bitmap mGainmapContents; 95 96 // called from JNI Gainmap(Bitmap gainmapContents, long nativeGainmap)97 private Gainmap(Bitmap gainmapContents, long nativeGainmap) { 98 if (nativeGainmap == 0) { 99 throw new RuntimeException("internal error: native gainmap is 0"); 100 } 101 102 mNativePtr = nativeGainmap; 103 setGainmapContents(gainmapContents); 104 105 NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, nativeGainmap); 106 } 107 108 /** 109 * Creates a gainmap from a given Bitmap. The caller is responsible for setting the various 110 * fields to the desired values. The defaults are as follows: 111 * <ul> 112 * <li>Ratio min is 1f, 1f, 1f</li> 113 * <li>Ratio max is 2f, 2f, 2f</li> 114 * <li>Gamma is 1f, 1f, 1f</li> 115 * <li>Epsilon SDR is 0f, 0f, 0f</li> 116 * <li>Epsilon HDR is 0f, 0f, 0f</li> 117 * <li>Display ratio SDR is 1f</li> 118 * <li>Display ratio HDR is 2f</li> 119 * </ul> 120 * It is strongly recommended that at least the ratio max and display ratio HDR are adjusted 121 * to better suit the given gainmap contents. 122 */ Gainmap(@onNull Bitmap gainmapContents)123 public Gainmap(@NonNull Bitmap gainmapContents) { 124 this(gainmapContents, nCreateEmpty()); 125 } 126 127 /** 128 * Creates a new gainmap using the provided gainmap as the metadata source and the provided 129 * bitmap as the replacement for the gainmapContents 130 */ 131 @FlaggedApi(Flags.FLAG_GAINMAP_CONSTRUCTOR_WITH_METADATA) Gainmap(@onNull Gainmap gainmap, @NonNull Bitmap gainmapContents)132 public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) { 133 this(gainmapContents, nCreateCopy(gainmap.mNativePtr)); 134 } 135 136 /** 137 * @return Returns the image data of the gainmap represented as a Bitmap. This is represented 138 * as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored 139 * such as {@link Bitmap#getColorSpace()} or {@link Bitmap#getGainmap()} as they are not 140 * relevant to the gainmap's enhancement layer. 141 */ 142 @NonNull getGainmapContents()143 public Bitmap getGainmapContents() { 144 return mGainmapContents; 145 } 146 147 /** 148 * Sets the image data of the gainmap. This is the 1 or 3 channel enhancement layer to apply 149 * to the base image. This is represented as a Bitmap for broad API compatibility, however 150 * certain aspects of the Bitmap are ignored such as {@link Bitmap#getColorSpace()} or 151 * {@link Bitmap#getGainmap()} as they are not relevant to the gainmap's enhancement layer. 152 * 153 * @param bitmap The non-null bitmap to set as the gainmap's contents 154 */ setGainmapContents(@onNull Bitmap bitmap)155 public void setGainmapContents(@NonNull Bitmap bitmap) { 156 // TODO: Validate here or leave native-side? 157 if (bitmap.isRecycled()) throw new IllegalArgumentException("Bitmap is recycled"); 158 nSetBitmap(mNativePtr, bitmap); 159 mGainmapContents = bitmap; 160 } 161 162 /** 163 * Sets the gainmap ratio min. For single-plane gainmaps, r, g, and b should be the same. 164 */ setRatioMin(float r, float g, float b)165 public void setRatioMin(float r, float g, float b) { 166 nSetRatioMin(mNativePtr, r, g, b); 167 } 168 169 /** 170 * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the 171 * same. The components are in r, g, b order. 172 */ 173 @NonNull getRatioMin()174 public float[] getRatioMin() { 175 float[] ret = new float[3]; 176 nGetRatioMin(mNativePtr, ret); 177 return ret; 178 } 179 180 /** 181 * Sets the gainmap ratio max. For single-plane gainmaps, r, g, and b should be the same. 182 */ setRatioMax(float r, float g, float b)183 public void setRatioMax(float r, float g, float b) { 184 nSetRatioMax(mNativePtr, r, g, b); 185 } 186 187 /** 188 * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the 189 * same. The components are in r, g, b order. 190 */ 191 @NonNull getRatioMax()192 public float[] getRatioMax() { 193 float[] ret = new float[3]; 194 nGetRatioMax(mNativePtr, ret); 195 return ret; 196 } 197 198 /** 199 * Sets the gainmap gamma. For single-plane gainmaps, r, g, and b should be the same. 200 */ setGamma(float r, float g, float b)201 public void setGamma(float r, float g, float b) { 202 nSetGamma(mNativePtr, r, g, b); 203 } 204 205 /** 206 * Gets the gainmap gamma. For single-plane gainmaps, all 3 components should be the 207 * same. The components are in r, g, b order. 208 */ 209 @NonNull getGamma()210 public float[] getGamma() { 211 float[] ret = new float[3]; 212 nGetGamma(mNativePtr, ret); 213 return ret; 214 } 215 216 /** 217 * Sets the sdr epsilon which is used to avoid numerical instability. 218 * For single-plane gainmaps, r, g, and b should be the same. 219 */ setEpsilonSdr(float r, float g, float b)220 public void setEpsilonSdr(float r, float g, float b) { 221 nSetEpsilonSdr(mNativePtr, r, g, b); 222 } 223 224 /** 225 * Gets the sdr epsilon. For single-plane gainmaps, all 3 components should be the 226 * same. The components are in r, g, b order. 227 */ 228 @NonNull getEpsilonSdr()229 public float[] getEpsilonSdr() { 230 float[] ret = new float[3]; 231 nGetEpsilonSdr(mNativePtr, ret); 232 return ret; 233 } 234 235 /** 236 * Sets the hdr epsilon which is used to avoid numerical instability. 237 * For single-plane gainmaps, r, g, and b should be the same. 238 */ setEpsilonHdr(float r, float g, float b)239 public void setEpsilonHdr(float r, float g, float b) { 240 nSetEpsilonHdr(mNativePtr, r, g, b); 241 } 242 243 /** 244 * Gets the hdr epsilon. For single-plane gainmaps, all 3 components should be the 245 * same. The components are in r, g, b order. 246 */ 247 @NonNull getEpsilonHdr()248 public float[] getEpsilonHdr() { 249 float[] ret = new float[3]; 250 nGetEpsilonHdr(mNativePtr, ret); 251 return ret; 252 } 253 254 /** 255 * Sets the hdr/sdr ratio at which point the gainmap is fully applied. 256 * @param max The hdr/sdr ratio at which the gainmap is fully applied. Must be >= 1.0f 257 */ setDisplayRatioForFullHdr(@loatRangefrom = 1.0f) float max)258 public void setDisplayRatioForFullHdr(@FloatRange(from = 1.0f) float max) { 259 if (!Float.isFinite(max) || max < 1f) { 260 throw new IllegalArgumentException( 261 "setDisplayRatioForFullHdr must be >= 1.0f, got = " + max); 262 } 263 nSetDisplayRatioHdr(mNativePtr, max); 264 } 265 266 /** 267 * Gets the hdr/sdr ratio at which point the gainmap is fully applied. 268 */ 269 @NonNull getDisplayRatioForFullHdr()270 public float getDisplayRatioForFullHdr() { 271 return nGetDisplayRatioHdr(mNativePtr); 272 } 273 274 /** 275 * Sets the hdr/sdr ratio below which only the SDR image is displayed. 276 * @param min The minimum hdr/sdr ratio at which to begin applying the gainmap. Must be >= 1.0f 277 */ setMinDisplayRatioForHdrTransition(@loatRangefrom = 1.0f) float min)278 public void setMinDisplayRatioForHdrTransition(@FloatRange(from = 1.0f) float min) { 279 if (!Float.isFinite(min) || min < 1f) { 280 throw new IllegalArgumentException( 281 "setMinDisplayRatioForHdrTransition must be >= 1.0f, got = " + min); 282 } 283 nSetDisplayRatioSdr(mNativePtr, min); 284 } 285 286 /** 287 * Gets the hdr/sdr ratio below which only the SDR image is displayed. 288 */ 289 @NonNull getMinDisplayRatioForHdrTransition()290 public float getMinDisplayRatioForHdrTransition() { 291 return nGetDisplayRatioSdr(mNativePtr); 292 } 293 294 /** 295 * No special parcel contents. 296 */ 297 @Override describeContents()298 public int describeContents() { 299 return 0; 300 } 301 302 /** 303 * Write the gainmap to the parcel. 304 * 305 * @param dest Parcel object to write the gainmap data into 306 * @param flags Additional flags about how the object should be written. 307 */ 308 @Override writeToParcel(@onNull Parcel dest, int flags)309 public void writeToParcel(@NonNull Parcel dest, int flags) { 310 if (mNativePtr == 0) { 311 throw new IllegalStateException("Cannot be written to a parcel"); 312 } 313 dest.writeTypedObject(mGainmapContents, flags); 314 // write gainmapinfo into parcel 315 nWriteGainmapToParcel(mNativePtr, dest); 316 } 317 318 public static final @NonNull Parcelable.Creator<Gainmap> CREATOR = 319 new Parcelable.Creator<Gainmap>() { 320 /** 321 * Rebuilds a gainmap previously stored with writeToParcel(). 322 * 323 * @param in Parcel object to read the gainmap from 324 * @return a new gainmap created from the data in the parcel 325 */ 326 public Gainmap createFromParcel(Parcel in) { 327 Gainmap gm = new Gainmap(in.readTypedObject(Bitmap.CREATOR)); 328 // read gainmapinfo from parcel 329 nReadGainmapFromParcel(gm.mNativePtr, in); 330 return gm; 331 } 332 333 public Gainmap[] newArray(int size) { 334 return new Gainmap[size]; 335 } 336 }; 337 nGetFinalizer()338 private static native long nGetFinalizer(); nCreateEmpty()339 private static native long nCreateEmpty(); nCreateCopy(long source)340 private static native long nCreateCopy(long source); 341 nSetBitmap(long ptr, Bitmap bitmap)342 private static native void nSetBitmap(long ptr, Bitmap bitmap); 343 nSetRatioMin(long ptr, float r, float g, float b)344 private static native void nSetRatioMin(long ptr, float r, float g, float b); nGetRatioMin(long ptr, float[] components)345 private static native void nGetRatioMin(long ptr, float[] components); 346 nSetRatioMax(long ptr, float r, float g, float b)347 private static native void nSetRatioMax(long ptr, float r, float g, float b); nGetRatioMax(long ptr, float[] components)348 private static native void nGetRatioMax(long ptr, float[] components); 349 nSetGamma(long ptr, float r, float g, float b)350 private static native void nSetGamma(long ptr, float r, float g, float b); nGetGamma(long ptr, float[] components)351 private static native void nGetGamma(long ptr, float[] components); 352 nSetEpsilonSdr(long ptr, float r, float g, float b)353 private static native void nSetEpsilonSdr(long ptr, float r, float g, float b); nGetEpsilonSdr(long ptr, float[] components)354 private static native void nGetEpsilonSdr(long ptr, float[] components); 355 nSetEpsilonHdr(long ptr, float r, float g, float b)356 private static native void nSetEpsilonHdr(long ptr, float r, float g, float b); nGetEpsilonHdr(long ptr, float[] components)357 private static native void nGetEpsilonHdr(long ptr, float[] components); 358 nSetDisplayRatioHdr(long ptr, float max)359 private static native void nSetDisplayRatioHdr(long ptr, float max); nGetDisplayRatioHdr(long ptr)360 private static native float nGetDisplayRatioHdr(long ptr); 361 nSetDisplayRatioSdr(long ptr, float min)362 private static native void nSetDisplayRatioSdr(long ptr, float min); nGetDisplayRatioSdr(long ptr)363 private static native float nGetDisplayRatioSdr(long ptr); nWriteGainmapToParcel(long ptr, Parcel dest)364 private static native void nWriteGainmapToParcel(long ptr, Parcel dest); nReadGainmapFromParcel(long ptr, Parcel src)365 private static native void nReadGainmapFromParcel(long ptr, Parcel src); 366 } 367