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.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 27 import com.android.graphics.hwui.flags.Flags; 28 29 import libcore.util.NativeAllocationRegistry; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 34 /** 35 * Gainmap represents a mechanism for augmenting an SDR image to produce an HDR one with variable 36 * display adjustment capability. It is a combination of a set of metadata describing how to apply 37 * the gainmap, as well as either a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3 38 * (such as {@link android.graphics.Bitmap.Config#ARGB_8888} with the alpha channel ignored) 39 * channel Bitmap that represents the gainmap data itself. 40 * <p> 41 * When rendering to an {@link android.content.pm.ActivityInfo#COLOR_MODE_HDR} activity, the 42 * hardware accelerated {@link Canvas} will automatically apply the gainmap when sufficient 43 * HDR headroom is available. 44 * 45 * <h3>Gainmap Structure</h3> 46 * 47 * The logical whole of a gainmap'd image consists of a base Bitmap that represents the original 48 * image as would be displayed without gainmap support in addition to a gainmap with a second 49 * enhancement image. In the case of a JPEG, the base image would be the typical 8-bit SDR image 50 * that the format is commonly associated with. The gainmap image is embedded alongside the base 51 * image, often at a lower resolution (such as 1/4th), along with some metadata to describe 52 * how to apply the gainmap. The gainmap image itself is then a greyscale image representing 53 * the transformation to apply onto the base image to reconstruct an HDR rendition of it. 54 * <p> 55 * As such these "gainmap images" consist of 3 parts - a base {@link Bitmap} with a 56 * {@link Bitmap#getGainmap()} that returns an instance of this class which in turn contains 57 * the enhancement layer represented as another Bitmap, accessible via {@link #getGainmapContents()} 58 * 59 * <h3>Applying a gainmap manually</h3> 60 * 61 * When doing custom rendering such as to an OpenGL ES or Vulkan context, the gainmap is not 62 * automatically applied. In such situations, the following steps are appropriate to render the 63 * gainmap in combination with the base image. 64 * <p> 65 * Suppose our display has HDR to SDR ratio of H, and we wish to display an image with gainmap on 66 * this display. Let B be the pixel value from the base image in a color space that has the 67 * primaries of the base image and a linear transfer function. Let G be the pixel value from the 68 * gainmap. Let D be the output pixel in the same color space as B. The value of D is computed 69 * as follows: 70 * <p> 71 * First, let W be a weight parameter determining how much the gainmap will be applied. 72 * <pre class="prettyprint"> 73 * W = clamp((log(H) - log(minDisplayRatioForHdrTransition)) / 74 * (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1)</pre> 75 * 76 * Next, let L be the gainmap value in log space. We compute this from the value G that was 77 * sampled from the texture as follows: 78 * <pre class="prettyprint"> 79 * L = mix(log(ratioMin), log(ratioMax), pow(G, gamma))</pre> 80 * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then 81 * compute: 82 * <pre class="prettyprint"> 83 * D = (B + epsilonSdr) * exp(L * W) - epsilonHdr</pre> 84 * <p> 85 * In the above math, log() is a natural logarithm and exp() is natural exponentiation. The base 86 * for these functions cancels out and does not affect the result, so other bases may be used 87 * if preferred. 88 */ 89 @android.ravenwood.annotation.RavenwoodKeepWholeClass 90 public final class Gainmap implements Parcelable { 91 92 /** @hide */ 93 @Retention(RetentionPolicy.SOURCE) 94 @IntDef(prefix = {"GAINMAP_DIRECTION_"}, 95 value = {GAINMAP_DIRECTION_SDR_TO_HDR, 96 GAINMAP_DIRECTION_HDR_TO_SDR}) 97 public @interface GainmapDirection {} 98 99 /** 100 * The gainmap will be applied as if the base image were SDR, and fully applying the gainmap 101 * results in an HDR image. 102 */ 103 @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS) 104 public static final int GAINMAP_DIRECTION_SDR_TO_HDR = 0; 105 106 /** 107 * The gainmap will be applied as if the base image were HDR, and fully applying the gainmap 108 * results in an SDR image. 109 */ 110 @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS) 111 public static final int GAINMAP_DIRECTION_HDR_TO_SDR = 1; 112 113 // Use a Holder to allow static initialization of Gainmap in the boot image. 114 private static class NoImagePreloadHolder { 115 public static final NativeAllocationRegistry sRegistry = 116 NativeAllocationRegistry.createMalloced( 117 Gainmap.class.getClassLoader(), nGetFinalizer()); 118 } 119 120 final long mNativePtr; 121 private Bitmap mGainmapContents; 122 123 // called from JNI Gainmap(Bitmap gainmapContents, long nativeGainmap)124 private Gainmap(Bitmap gainmapContents, long nativeGainmap) { 125 if (nativeGainmap == 0) { 126 throw new RuntimeException("internal error: native gainmap is 0"); 127 } 128 129 mNativePtr = nativeGainmap; 130 setGainmapContents(gainmapContents); 131 132 NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, nativeGainmap); 133 } 134 135 /** 136 * Creates a gainmap from a given Bitmap. The caller is responsible for setting the various 137 * fields to the desired values. The defaults are as follows: 138 * <ul> 139 * <li>Ratio min is 1f, 1f, 1f</li> 140 * <li>Ratio max is 2f, 2f, 2f</li> 141 * <li>Gamma is 1f, 1f, 1f</li> 142 * <li>Epsilon SDR is 0f, 0f, 0f</li> 143 * <li>Epsilon HDR is 0f, 0f, 0f</li> 144 * <li>Display ratio SDR is 1f</li> 145 * <li>Display ratio HDR is 2f</li> 146 * </ul> 147 * It is strongly recommended that at least the ratio max and display ratio HDR are adjusted 148 * to better suit the given gainmap contents. 149 */ Gainmap(@onNull Bitmap gainmapContents)150 public Gainmap(@NonNull Bitmap gainmapContents) { 151 this(gainmapContents, nCreateEmpty()); 152 } 153 154 /** 155 * Creates a new gainmap using the provided gainmap as the metadata source and the provided 156 * bitmap as the replacement for the gainmapContents 157 */ 158 @FlaggedApi(Flags.FLAG_GAINMAP_CONSTRUCTOR_WITH_METADATA) Gainmap(@onNull Gainmap gainmap, @NonNull Bitmap gainmapContents)159 public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) { 160 this(gainmapContents, nCreateCopy(gainmap.mNativePtr)); 161 } 162 163 /** 164 * @hide 165 */ asShared()166 public Gainmap asShared() { 167 final Bitmap sharedContents = mGainmapContents.asShared(); 168 if (sharedContents == mGainmapContents) { 169 return this; 170 } else { 171 return new Gainmap(sharedContents, nCreateCopy(mNativePtr)); 172 } 173 } 174 175 /** 176 * @return Returns the image data of the gainmap represented as a Bitmap. This is represented 177 * as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored 178 * such as {@link Bitmap#getColorSpace()} or {@link Bitmap#getGainmap()} as they are not 179 * relevant to the gainmap's enhancement layer. 180 */ 181 @NonNull getGainmapContents()182 public Bitmap getGainmapContents() { 183 return mGainmapContents; 184 } 185 186 /** 187 * Sets the image data of the gainmap. This is the 1 or 3 channel enhancement layer to apply 188 * to the base image. This is represented as a Bitmap for broad API compatibility, however 189 * certain aspects of the Bitmap are ignored such as {@link Bitmap#getColorSpace()} or 190 * {@link Bitmap#getGainmap()} as they are not relevant to the gainmap's enhancement layer. 191 * 192 * @param bitmap The non-null bitmap to set as the gainmap's contents 193 */ setGainmapContents(@onNull Bitmap bitmap)194 public void setGainmapContents(@NonNull Bitmap bitmap) { 195 // TODO: Validate here or leave native-side? 196 if (bitmap.isRecycled()) throw new IllegalArgumentException("Bitmap is recycled"); 197 nSetBitmap(mNativePtr, bitmap); 198 mGainmapContents = bitmap; 199 } 200 201 /** 202 * Sets the gainmap ratio min. For single-plane gainmaps, r, g, and b should be the same. 203 */ setRatioMin(float r, float g, float b)204 public void setRatioMin(float r, float g, float b) { 205 nSetRatioMin(mNativePtr, r, g, b); 206 } 207 208 /** 209 * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the 210 * same. The components are in r, g, b order. 211 */ 212 @NonNull getRatioMin()213 public float[] getRatioMin() { 214 float[] ret = new float[3]; 215 nGetRatioMin(mNativePtr, ret); 216 return ret; 217 } 218 219 /** 220 * Sets the gainmap ratio max. For single-plane gainmaps, r, g, and b should be the same. 221 */ setRatioMax(float r, float g, float b)222 public void setRatioMax(float r, float g, float b) { 223 nSetRatioMax(mNativePtr, r, g, b); 224 } 225 226 /** 227 * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the 228 * same. The components are in r, g, b order. 229 */ 230 @NonNull getRatioMax()231 public float[] getRatioMax() { 232 float[] ret = new float[3]; 233 nGetRatioMax(mNativePtr, ret); 234 return ret; 235 } 236 237 /** 238 * Sets the gainmap gamma. For single-plane gainmaps, r, g, and b should be the same. 239 */ setGamma(float r, float g, float b)240 public void setGamma(float r, float g, float b) { 241 nSetGamma(mNativePtr, r, g, b); 242 } 243 244 /** 245 * Gets the gainmap gamma. For single-plane gainmaps, all 3 components should be the 246 * same. The components are in r, g, b order. 247 */ 248 @NonNull getGamma()249 public float[] getGamma() { 250 float[] ret = new float[3]; 251 nGetGamma(mNativePtr, ret); 252 return ret; 253 } 254 255 /** 256 * Sets the sdr epsilon which is used to avoid numerical instability. 257 * For single-plane gainmaps, r, g, and b should be the same. 258 */ setEpsilonSdr(float r, float g, float b)259 public void setEpsilonSdr(float r, float g, float b) { 260 nSetEpsilonSdr(mNativePtr, r, g, b); 261 } 262 263 /** 264 * Gets the sdr epsilon. For single-plane gainmaps, all 3 components should be the 265 * same. The components are in r, g, b order. 266 */ 267 @NonNull getEpsilonSdr()268 public float[] getEpsilonSdr() { 269 float[] ret = new float[3]; 270 nGetEpsilonSdr(mNativePtr, ret); 271 return ret; 272 } 273 274 /** 275 * Sets the hdr epsilon which is used to avoid numerical instability. 276 * For single-plane gainmaps, r, g, and b should be the same. 277 */ setEpsilonHdr(float r, float g, float b)278 public void setEpsilonHdr(float r, float g, float b) { 279 nSetEpsilonHdr(mNativePtr, r, g, b); 280 } 281 282 /** 283 * Gets the hdr epsilon. For single-plane gainmaps, all 3 components should be the 284 * same. The components are in r, g, b order. 285 */ 286 @NonNull getEpsilonHdr()287 public float[] getEpsilonHdr() { 288 float[] ret = new float[3]; 289 nGetEpsilonHdr(mNativePtr, ret); 290 return ret; 291 } 292 293 /** 294 * Sets the hdr/sdr ratio at which point applying the gainmap results in an HDR rendition. 295 * @param max The hdr/sdr ratio at which point applying the gainmap results in an HDR rendition. 296 * Must be >= 1.0f 297 */ setDisplayRatioForFullHdr(@loatRangefrom = 1.0f) float max)298 public void setDisplayRatioForFullHdr(@FloatRange(from = 1.0f) float max) { 299 if (!Float.isFinite(max) || max < 1f) { 300 throw new IllegalArgumentException( 301 "setDisplayRatioForFullHdr must be >= 1.0f, got = " + max); 302 } 303 nSetDisplayRatioHdr(mNativePtr, max); 304 } 305 306 /** 307 * Gets the hdr/sdr ratio at which point applying the gainmap results in an HDR rendition 308 */ 309 @NonNull getDisplayRatioForFullHdr()310 public float getDisplayRatioForFullHdr() { 311 return nGetDisplayRatioHdr(mNativePtr); 312 } 313 314 /** 315 * Sets the hdr/sdr ratio below which applying the gainmap results in an SDR rendition. 316 * @param min The minimum hdr/sdr ratio at which point applying the gainmap results in an SDR 317 * rendition. Must be >= 1.0f 318 */ setMinDisplayRatioForHdrTransition(@loatRangefrom = 1.0f) float min)319 public void setMinDisplayRatioForHdrTransition(@FloatRange(from = 1.0f) float min) { 320 if (!Float.isFinite(min) || min < 1f) { 321 throw new IllegalArgumentException( 322 "setMinDisplayRatioForHdrTransition must be >= 1.0f, got = " + min); 323 } 324 nSetDisplayRatioSdr(mNativePtr, min); 325 } 326 327 /** 328 * Gets the hdr/sdr ratio below which applying the gainmap results in an SDR rendition. 329 */ 330 @NonNull getMinDisplayRatioForHdrTransition()331 public float getMinDisplayRatioForHdrTransition() { 332 return nGetDisplayRatioSdr(mNativePtr); 333 } 334 335 /** 336 * Sets the colorspace that the gainmap math should be applied in. 337 * Only the primaries are what is relevant for applying the gainmap. The transfer and range 338 * characteritics are ignored. 339 * 340 * If the supplied ColorSpace is null, then applying the gainmap will be done using the color 341 * gamut of the base image. 342 */ 343 @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS) setAlternativeImagePrimaries(@ullable ColorSpace colorSpace)344 public void setAlternativeImagePrimaries(@Nullable ColorSpace colorSpace) { 345 long colorSpaceInstance = colorSpace == null ? 0 : colorSpace.getNativeInstance(); 346 nSetAlternativeColorSpace(mNativePtr, colorSpaceInstance); 347 } 348 349 /** 350 * Gets the colorspace that the gainmap math should be applied in. 351 * Only the primaries are what is relevant for applying the gainmap. The transfer and range 352 * characteritics are ignored. 353 * 354 * If the returned ColorSpace is null, then applying the gainmap will be done using the color 355 * gamut of the base image. 356 */ 357 @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS) 358 @Nullable getAlternativeImagePrimaries()359 public ColorSpace getAlternativeImagePrimaries() { 360 return nGetAlternativeColorSpace(mNativePtr); 361 } 362 363 /** 364 * Sets the direction that the gainmap math should be applied in. 365 */ 366 @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS) setGainmapDirection(@ainmapDirection int direction)367 public void setGainmapDirection(@GainmapDirection int direction) { 368 if (direction != GAINMAP_DIRECTION_SDR_TO_HDR 369 && direction != GAINMAP_DIRECTION_HDR_TO_SDR) { 370 throw new IllegalArgumentException("Invalid gainmap direction: " + direction); 371 } 372 nSetDirection(mNativePtr, direction); 373 } 374 375 /** 376 * Gets the direction that the gainmap math should be applied in. 377 */ 378 @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS) getGainmapDirection()379 public @GainmapDirection int getGainmapDirection() { 380 return nGetDirection(mNativePtr); 381 } 382 383 384 /** 385 * No special parcel contents. 386 */ 387 @Override describeContents()388 public int describeContents() { 389 return 0; 390 } 391 392 /** 393 * Write the gainmap to the parcel. 394 * 395 * @param dest Parcel object to write the gainmap data into 396 * @param flags Additional flags about how the object should be written. 397 */ 398 @Override writeToParcel(@onNull Parcel dest, int flags)399 public void writeToParcel(@NonNull Parcel dest, int flags) { 400 if (mNativePtr == 0) { 401 throw new IllegalStateException("Cannot be written to a parcel"); 402 } 403 dest.writeTypedObject(mGainmapContents, flags); 404 // write gainmapinfo into parcel 405 nWriteGainmapToParcel(mNativePtr, dest); 406 } 407 408 public static final @NonNull Parcelable.Creator<Gainmap> CREATOR = 409 new Parcelable.Creator<Gainmap>() { 410 /** 411 * Rebuilds a gainmap previously stored with writeToParcel(). 412 * 413 * @param in Parcel object to read the gainmap from 414 * @return a new gainmap created from the data in the parcel 415 */ 416 public Gainmap createFromParcel(Parcel in) { 417 Gainmap gm = new Gainmap(in.readTypedObject(Bitmap.CREATOR)); 418 // read gainmapinfo from parcel 419 nReadGainmapFromParcel(gm.mNativePtr, in); 420 return gm; 421 } 422 423 public Gainmap[] newArray(int size) { 424 return new Gainmap[size]; 425 } 426 }; 427 nGetFinalizer()428 private static native long nGetFinalizer(); nCreateEmpty()429 private static native long nCreateEmpty(); nCreateCopy(long source)430 private static native long nCreateCopy(long source); 431 nSetBitmap(long ptr, Bitmap bitmap)432 private static native void nSetBitmap(long ptr, Bitmap bitmap); 433 nSetRatioMin(long ptr, float r, float g, float b)434 private static native void nSetRatioMin(long ptr, float r, float g, float b); nGetRatioMin(long ptr, float[] components)435 private static native void nGetRatioMin(long ptr, float[] components); 436 nSetRatioMax(long ptr, float r, float g, float b)437 private static native void nSetRatioMax(long ptr, float r, float g, float b); nGetRatioMax(long ptr, float[] components)438 private static native void nGetRatioMax(long ptr, float[] components); 439 nSetGamma(long ptr, float r, float g, float b)440 private static native void nSetGamma(long ptr, float r, float g, float b); nGetGamma(long ptr, float[] components)441 private static native void nGetGamma(long ptr, float[] components); 442 nSetEpsilonSdr(long ptr, float r, float g, float b)443 private static native void nSetEpsilonSdr(long ptr, float r, float g, float b); nGetEpsilonSdr(long ptr, float[] components)444 private static native void nGetEpsilonSdr(long ptr, float[] components); 445 nSetEpsilonHdr(long ptr, float r, float g, float b)446 private static native void nSetEpsilonHdr(long ptr, float r, float g, float b); nGetEpsilonHdr(long ptr, float[] components)447 private static native void nGetEpsilonHdr(long ptr, float[] components); 448 nSetDisplayRatioHdr(long ptr, float max)449 private static native void nSetDisplayRatioHdr(long ptr, float max); nGetDisplayRatioHdr(long ptr)450 private static native float nGetDisplayRatioHdr(long ptr); 451 nSetDisplayRatioSdr(long ptr, float min)452 private static native void nSetDisplayRatioSdr(long ptr, float min); nGetDisplayRatioSdr(long ptr)453 private static native float nGetDisplayRatioSdr(long ptr); nSetAlternativeColorSpace(long ptr, long colorSpacePtr)454 private static native void nSetAlternativeColorSpace(long ptr, long colorSpacePtr); nGetAlternativeColorSpace(long ptr)455 private static native ColorSpace nGetAlternativeColorSpace(long ptr); nSetDirection(long ptr, int direction)456 private static native void nSetDirection(long ptr, int direction); nGetDirection(long ptr)457 private static native int nGetDirection(long ptr); nWriteGainmapToParcel(long ptr, Parcel dest)458 private static native void nWriteGainmapToParcel(long ptr, Parcel dest); nReadGainmapFromParcel(long ptr, Parcel src)459 private static native void nReadGainmapFromParcel(long ptr, Parcel src); 460 } 461