1 /*
2  * Copyright (C) 2019 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 com.android.server.display.color;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import static com.android.server.display.color.DisplayTransformManager.LEVEL_COLOR_MATRIX_DISPLAY_WHITE_BALANCE;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.Size;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.graphics.ColorSpace;
29 import android.hardware.display.ColorDisplayManager;
30 import android.hardware.display.DisplayManagerInternal;
31 import android.opengl.Matrix;
32 import android.util.Slog;
33 import android.view.SurfaceControl.DisplayPrimaries;
34 
35 import com.android.internal.R;
36 import com.android.internal.annotations.VisibleForTesting;
37 
38 import java.io.PrintWriter;
39 
40 final class DisplayWhiteBalanceTintController extends ColorTemperatureTintController {
41 
42     // Three chromaticity coordinates per color: X, Y, and Z
43     private static final int NUM_VALUES_PER_PRIMARY = 3;
44     // Four colors: red, green, blue, and white
45     private static final int NUM_DISPLAY_PRIMARIES_VALS = 4 * NUM_VALUES_PER_PRIMARY;
46     private static final int COLORSPACE_MATRIX_LENGTH = 9;
47 
48     private final Object mLock = new Object();
49     @VisibleForTesting
50     int mTemperatureMin;
51     @VisibleForTesting
52     int mTemperatureMax;
53     private int mTemperatureDefault;
54     @VisibleForTesting
55     float[] mDisplayNominalWhiteXYZ = new float[NUM_VALUES_PER_PRIMARY];
56     private int mDisplayNominalWhiteCct;
57     @VisibleForTesting
58     ColorSpace.Rgb mDisplayColorSpaceRGB;
59     private float[] mChromaticAdaptationMatrix;
60     // The temperature currently represented in the matrix.
61     @VisibleForTesting
62     int mCurrentColorTemperature;
63     private float[] mCurrentColorTemperatureXYZ;
64     @VisibleForTesting
65     boolean mSetUp = false;
66     private final float[] mMatrixDisplayWhiteBalance = new float[16];
67     private long mTransitionDuration;
68     private Boolean mIsAvailable;
69     // This feature becomes disallowed if the device is in an unsupported strong/light state.
70     private boolean mIsAllowed = true;
71     private int mTargetCct;
72     private int mAppliedCct;
73     private CctEvaluator mCctEvaluator;
74 
75     private final DisplayManagerInternal mDisplayManagerInternal;
76 
DisplayWhiteBalanceTintController(DisplayManagerInternal dm)77     DisplayWhiteBalanceTintController(DisplayManagerInternal dm) {
78         mDisplayManagerInternal = dm;
79     }
80 
81     @Override
setUp(Context context, boolean needsLinear)82     public void setUp(Context context, boolean needsLinear) {
83         mSetUp = false;
84         final Resources res = context.getResources();
85 
86         // Initialize with the config value for light mode, so it starts in the right state.
87         setAllowed(res.getBoolean(R.bool.config_displayWhiteBalanceLightModeAllowed));
88 
89         ColorSpace.Rgb displayColorSpaceRGB = getDisplayColorSpaceFromSurfaceControl();
90         if (displayColorSpaceRGB == null) {
91             Slog.w(ColorDisplayService.TAG,
92                     "Failed to get display color space from SurfaceControl, trying res");
93             displayColorSpaceRGB = getDisplayColorSpaceFromResources(res);
94             if (displayColorSpaceRGB == null) {
95                 Slog.e(ColorDisplayService.TAG, "Failed to get display color space from resources");
96                 return;
97             }
98         }
99 
100         // Make sure display color space is valid
101         if (!isColorMatrixValid(displayColorSpaceRGB.getTransform())) {
102             Slog.e(ColorDisplayService.TAG, "Invalid display color space RGB-to-XYZ transform");
103             return;
104         }
105         if (!isColorMatrixValid(displayColorSpaceRGB.getInverseTransform())) {
106             Slog.e(ColorDisplayService.TAG, "Invalid display color space XYZ-to-RGB transform");
107             return;
108         }
109 
110         final String[] nominalWhiteValues = res.getStringArray(
111                 R.array.config_displayWhiteBalanceDisplayNominalWhite);
112         float[] displayNominalWhiteXYZ = new float[NUM_VALUES_PER_PRIMARY];
113         for (int i = 0; i < nominalWhiteValues.length; i++) {
114             displayNominalWhiteXYZ[i] = Float.parseFloat(nominalWhiteValues[i]);
115         }
116 
117         final int displayNominalWhiteCct = res.getInteger(
118                 R.integer.config_displayWhiteBalanceDisplayNominalWhiteCct);
119 
120         final int colorTemperatureMin = res.getInteger(
121                 R.integer.config_displayWhiteBalanceColorTemperatureMin);
122         if (colorTemperatureMin <= 0) {
123             Slog.e(ColorDisplayService.TAG,
124                     "Display white balance minimum temperature must be greater than 0");
125             return;
126         }
127 
128         final int colorTemperatureMax = res.getInteger(
129                 R.integer.config_displayWhiteBalanceColorTemperatureMax);
130         if (colorTemperatureMax < colorTemperatureMin) {
131             Slog.e(ColorDisplayService.TAG,
132                     "Display white balance max temp must be greater or equal to min");
133             return;
134         }
135 
136         final int defaultTemperature = res.getInteger(
137                 R.integer.config_displayWhiteBalanceColorTemperatureDefault);
138 
139         mTransitionDuration = res.getInteger(
140                 R.integer.config_displayWhiteBalanceTransitionTime);
141 
142         int[] cctRangeMinimums = res.getIntArray(
143                 R.array.config_displayWhiteBalanceDisplayRangeMinimums);
144         int[] steps = res.getIntArray(R.array.config_displayWhiteBalanceDisplaySteps);
145 
146         synchronized (mLock) {
147             mDisplayColorSpaceRGB = displayColorSpaceRGB;
148             mDisplayNominalWhiteXYZ = displayNominalWhiteXYZ;
149             mDisplayNominalWhiteCct = displayNominalWhiteCct;
150             mTargetCct = mDisplayNominalWhiteCct;
151             mAppliedCct = mDisplayNominalWhiteCct;
152             mTemperatureMin = colorTemperatureMin;
153             mTemperatureMax = colorTemperatureMax;
154             mTemperatureDefault = defaultTemperature;
155             mSetUp = true;
156             mCctEvaluator = new CctEvaluator(mTemperatureMin, mTemperatureMax,
157                     cctRangeMinimums, steps);
158         }
159 
160         setMatrix(mTemperatureDefault);
161     }
162 
163     @Override
getMatrix()164     public float[] getMatrix() {
165         if (!mSetUp || !isActivated()) {
166             return ColorDisplayService.MATRIX_IDENTITY;
167         }
168         computeMatrixForCct(mAppliedCct);
169         return mMatrixDisplayWhiteBalance;
170     }
171 
172     @Override
getTargetCct()173     public int getTargetCct() {
174         return mTargetCct;
175     }
176 
177     /**
178      * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats.
179      *
180      * @param lhs 3x3 matrix, as a non-null array of 9 floats
181      * @param rhs 3x3 matrix, as a non-null array of 9 floats
182      * @return A new array of 9 floats containing the result of the multiplication
183      *         of rhs by lhs
184      */
185     @NonNull
186     @Size(9)
mul3x3(@onNull @ize9) float[] lhs, @NonNull @Size(9) float[] rhs)187     private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) {
188         float[] r = new float[9];
189         r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2];
190         r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2];
191         r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2];
192         r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5];
193         r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5];
194         r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5];
195         r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8];
196         r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8];
197         r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8];
198         return r;
199     }
200 
201     @Override
setMatrix(int cct)202     public void setMatrix(int cct) {
203         setTargetCct(cct);
204         computeMatrixForCct(mTargetCct);
205     }
206 
207     @Override
setTargetCct(int cct)208     public void setTargetCct(int cct) {
209         if (!mSetUp) {
210             Slog.w(ColorDisplayService.TAG,
211                     "Can't set display white balance temperature: uninitialized");
212             return;
213         }
214 
215         if (cct < mTemperatureMin) {
216             Slog.w(ColorDisplayService.TAG,
217                     "Requested display color temperature is below allowed minimum");
218             mTargetCct = mTemperatureMin;
219         } else if (cct > mTemperatureMax) {
220             Slog.w(ColorDisplayService.TAG,
221                     "Requested display color temperature is above allowed maximum");
222             mTargetCct = mTemperatureMax;
223         } else {
224             mTargetCct = cct;
225         }
226     }
227 
228     @Override
getDisabledCct()229     public int getDisabledCct() {
230         return mDisplayNominalWhiteCct;
231     }
232 
233     @Override
computeMatrixForCct(int cct)234     public float[] computeMatrixForCct(int cct) {
235         if (!mSetUp || cct == 0) {
236             Slog.w(ColorDisplayService.TAG, "Couldn't compute matrix for cct=" + cct);
237             return ColorDisplayService.MATRIX_IDENTITY;
238         }
239 
240         synchronized (mLock) {
241             mCurrentColorTemperature = cct;
242 
243             if (cct == mDisplayNominalWhiteCct && !isActivated()) {
244                 // DWB is finished turning off. Clear the matrix.
245                 Matrix.setIdentityM(mMatrixDisplayWhiteBalance, 0);
246             } else {
247                 computeMatrixForCctLocked(cct);
248             }
249 
250             Slog.d(ColorDisplayService.TAG, "computeDisplayWhiteBalanceMatrix: cct =" + cct
251                     + " matrix =" + matrixToString(mMatrixDisplayWhiteBalance, 16));
252 
253             return mMatrixDisplayWhiteBalance;
254         }
255     }
256 
computeMatrixForCctLocked(int cct)257     private void computeMatrixForCctLocked(int cct) {
258         // Adapt the display's nominal white point to match the requested CCT value
259         mCurrentColorTemperatureXYZ = ColorSpace.cctToXyz(cct);
260 
261         mChromaticAdaptationMatrix =
262                 ColorSpace.chromaticAdaptation(ColorSpace.Adaptation.BRADFORD,
263                         mDisplayNominalWhiteXYZ, mCurrentColorTemperatureXYZ);
264 
265         // Convert the adaptation matrix to RGB space
266         float[] result = mul3x3(mChromaticAdaptationMatrix,
267                 mDisplayColorSpaceRGB.getTransform());
268         result = mul3x3(mDisplayColorSpaceRGB.getInverseTransform(), result);
269 
270         // Normalize the transform matrix to peak white value in RGB space
271         final float adaptedMaxR = result[0] + result[3] + result[6];
272         final float adaptedMaxG = result[1] + result[4] + result[7];
273         final float adaptedMaxB = result[2] + result[5] + result[8];
274         final float denum = Math.max(Math.max(adaptedMaxR, adaptedMaxG), adaptedMaxB);
275 
276         Matrix.setIdentityM(mMatrixDisplayWhiteBalance, 0);
277 
278         for (int i = 0; i < result.length; i++) {
279             result[i] /= denum;
280             if (!isColorMatrixCoeffValid(result[i])) {
281                 Slog.e(ColorDisplayService.TAG, "Invalid DWB color matrix");
282                 return;
283             }
284         }
285 
286         java.lang.System.arraycopy(result, 0, mMatrixDisplayWhiteBalance, 0, 3);
287         java.lang.System.arraycopy(result, 3, mMatrixDisplayWhiteBalance, 4, 3);
288         java.lang.System.arraycopy(result, 6, mMatrixDisplayWhiteBalance, 8, 3);
289     }
290 
291     @Override
getAppliedCct()292     int getAppliedCct() {
293         return mAppliedCct;
294     }
295 
296     @Override
setAppliedCct(int cct)297     void setAppliedCct(int cct) {
298         mAppliedCct = cct;
299     }
300 
301     @Override
302     @Nullable
getEvaluator()303     CctEvaluator getEvaluator() {
304         return mCctEvaluator;
305     }
306 
307     @Override
getLevel()308     public int getLevel() {
309         return LEVEL_COLOR_MATRIX_DISPLAY_WHITE_BALANCE;
310     }
311 
312     @Override
isAvailable(Context context)313     public boolean isAvailable(Context context) {
314         if (mIsAvailable == null) {
315             mIsAvailable = ColorDisplayManager.isDisplayWhiteBalanceAvailable(context);
316         }
317         return mIsAvailable;
318     }
319 
320     @Override
getTransitionDurationMilliseconds()321     public long getTransitionDurationMilliseconds() {
322         return mTransitionDuration;
323     }
324 
325     @Override
dump(PrintWriter pw)326     public void dump(PrintWriter pw) {
327         synchronized (mLock) {
328             pw.println("    mSetUp = " + mSetUp);
329             if (!mSetUp) {
330                 return;
331             }
332 
333             pw.println("    mTemperatureMin = " + mTemperatureMin);
334             pw.println("    mTemperatureMax = " + mTemperatureMax);
335             pw.println("    mTemperatureDefault = " + mTemperatureDefault);
336             pw.println("    mDisplayNominalWhiteCct = " + mDisplayNominalWhiteCct);
337             pw.println("    mCurrentColorTemperature = " + mCurrentColorTemperature);
338             pw.println("    mTargetCct = " + mTargetCct);
339             pw.println("    mAppliedCct = " + mAppliedCct);
340             pw.println("    mCurrentColorTemperatureXYZ = "
341                     + matrixToString(mCurrentColorTemperatureXYZ, 3));
342             pw.println("    mDisplayColorSpaceRGB RGB-to-XYZ = "
343                     + matrixToString(mDisplayColorSpaceRGB.getTransform(), 3));
344             pw.println("    mChromaticAdaptationMatrix = "
345                     + matrixToString(mChromaticAdaptationMatrix, 3));
346             pw.println("    mDisplayColorSpaceRGB XYZ-to-RGB = "
347                     + matrixToString(mDisplayColorSpaceRGB.getInverseTransform(), 3));
348             pw.println("    mMatrixDisplayWhiteBalance = "
349                     + matrixToString(mMatrixDisplayWhiteBalance, 4));
350             pw.println("    mIsAllowed = " + mIsAllowed);
351         }
352     }
353 
getLuminance()354     public float getLuminance() {
355         synchronized (mLock) {
356             if (mChromaticAdaptationMatrix != null && mChromaticAdaptationMatrix.length == 9) {
357                 // Compute only the luminance (y) value of the xyz * [1 1 1] transform.
358                 return 1 / (mChromaticAdaptationMatrix[1] + mChromaticAdaptationMatrix[4]
359                         + mChromaticAdaptationMatrix[7]);
360             } else {
361                 return -1;
362             }
363         }
364     }
365 
setAllowed(boolean allowed)366     public void setAllowed(boolean allowed) {
367         mIsAllowed = allowed;
368     }
369 
isAllowed()370     public boolean isAllowed() {
371         return mIsAllowed;
372     }
373 
makeRgbColorSpaceFromXYZ(float[] redGreenBlueXYZ, float[] whiteXYZ)374     private ColorSpace.Rgb makeRgbColorSpaceFromXYZ(float[] redGreenBlueXYZ, float[] whiteXYZ) {
375         return new ColorSpace.Rgb(
376                 "Display Color Space",
377                 redGreenBlueXYZ,
378                 whiteXYZ,
379                 2.2f // gamma, unused for display white balance
380         );
381     }
382 
getDisplayColorSpaceFromSurfaceControl()383     private ColorSpace.Rgb getDisplayColorSpaceFromSurfaceControl() {
384         DisplayPrimaries primaries =
385                 mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY);
386         if (primaries == null || primaries.red == null || primaries.green == null
387                 || primaries.blue == null || primaries.white == null) {
388             return null;
389         }
390 
391         return makeRgbColorSpaceFromXYZ(
392                 new float[]{
393                         primaries.red.X, primaries.red.Y, primaries.red.Z,
394                         primaries.green.X, primaries.green.Y, primaries.green.Z,
395                         primaries.blue.X, primaries.blue.Y, primaries.blue.Z,
396                 },
397                 new float[]{primaries.white.X, primaries.white.Y, primaries.white.Z}
398         );
399     }
400 
getDisplayColorSpaceFromResources(Resources res)401     private ColorSpace.Rgb getDisplayColorSpaceFromResources(Resources res) {
402         final String[] displayPrimariesValues = res.getStringArray(
403                 R.array.config_displayWhiteBalanceDisplayPrimaries);
404         float[] displayRedGreenBlueXYZ =
405                 new float[NUM_DISPLAY_PRIMARIES_VALS - NUM_VALUES_PER_PRIMARY];
406         float[] displayWhiteXYZ = new float[NUM_VALUES_PER_PRIMARY];
407 
408         for (int i = 0; i < displayRedGreenBlueXYZ.length; i++) {
409             displayRedGreenBlueXYZ[i] = Float.parseFloat(displayPrimariesValues[i]);
410         }
411 
412         for (int i = 0; i < displayWhiteXYZ.length; i++) {
413             displayWhiteXYZ[i] = Float.parseFloat(
414                     displayPrimariesValues[displayRedGreenBlueXYZ.length + i]);
415         }
416 
417         return makeRgbColorSpaceFromXYZ(displayRedGreenBlueXYZ, displayWhiteXYZ);
418     }
419 
isColorMatrixCoeffValid(float coeff)420     private boolean isColorMatrixCoeffValid(float coeff) {
421         return !Float.isNaN(coeff) && !Float.isInfinite(coeff);
422     }
423 
isColorMatrixValid(float[] matrix)424     private boolean isColorMatrixValid(float[] matrix) {
425         if (matrix == null || matrix.length != COLORSPACE_MATRIX_LENGTH) {
426             return false;
427         }
428 
429         for (float value : matrix) {
430             if (!isColorMatrixCoeffValid(value)) {
431                 return false;
432             }
433         }
434 
435         return true;
436     }
437 
438 }
439