Introducing HDRClamper
Bug: b/283447291
Test: atest HdrClamperTest, see bug comment 9 for manual testing
Change-Id: I9e0ae3a02cd9427644ea133a131010ff5a4763ae
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 4bfc090..64d2314 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -17,9 +17,11 @@
package com.android.server.display;
import android.hardware.display.BrightnessInfo;
+import android.os.Handler;
import android.os.IBinder;
import android.provider.DeviceConfigInterface;
+import com.android.server.display.brightness.clamper.HdrClamper;
import com.android.server.display.feature.DeviceConfigParameterProvider;
import java.io.PrintWriter;
@@ -31,23 +33,30 @@
private final NormalBrightnessModeController mNormalBrightnessModeController =
new NormalBrightnessModeController();
+ private final HdrClamper mHdrClamper;
+
private final Runnable mModeChangeCallback;
private final boolean mUseNbmController;
+ private final boolean mUseHdrClamper;
+
BrightnessRangeController(HighBrightnessModeController hbmController,
- Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig) {
+ Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig, Handler handler) {
this(hbmController, modeChangeCallback, displayDeviceConfig,
+ new HdrClamper(modeChangeCallback::run, new Handler(handler.getLooper())),
new DeviceConfigParameterProvider(DeviceConfigInterface.REAL));
}
BrightnessRangeController(HighBrightnessModeController hbmController,
Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig,
- DeviceConfigParameterProvider configParameterProvider) {
+ HdrClamper hdrClamper, DeviceConfigParameterProvider configParameterProvider) {
mHbmController = hbmController;
mModeChangeCallback = modeChangeCallback;
mUseNbmController = configParameterProvider.isNormalBrightnessControllerFeatureEnabled();
+ mUseHdrClamper = false;
mNormalBrightnessModeController.resetNbmData(displayDeviceConfig.getLuxThrottlingData());
+ mHdrClamper = hdrClamper;
}
void dump(PrintWriter pw) {
@@ -63,6 +72,9 @@
() -> mNormalBrightnessModeController.onAmbientLuxChange(ambientLux),
() -> mHbmController.onAmbientLuxChange(ambientLux)
);
+ if (mUseHdrClamper) {
+ mHdrClamper.onAmbientLuxChange(ambientLux);
+ }
}
float getNormalBrightnessMax() {
@@ -118,7 +130,8 @@
}
float getHdrBrightnessValue() {
- return mHbmController.getHdrBrightnessValue();
+ float hdrBrightness = mHbmController.getHdrBrightnessValue();
+ return Math.min(hdrBrightness, mHdrClamper.getMaxBrightness());
}
float getTransitionPoint() {
@@ -138,4 +151,8 @@
hbmChangesFunc.run();
}
}
+
+ public float getHdrTransitionRate() {
+ return mHdrClamper.getTransitionRate();
+ }
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 40dbabf..b7b46ea 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -677,7 +677,7 @@
HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback);
mBrightnessRangeController = new BrightnessRangeController(hbmController,
- modeChangeCallback, mDisplayDeviceConfig);
+ modeChangeCallback, mDisplayDeviceConfig, mHandler);
mBrightnessThrottler = createBrightnessThrottlerLocked();
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 460c351..7021eed 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -539,8 +539,8 @@
modeChangeCallback);
mBrightnessThrottler = createBrightnessThrottlerLocked();
- mBrightnessRangeController = new BrightnessRangeController(hbmController,
- modeChangeCallback, mDisplayDeviceConfig);
+ mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
+ modeChangeCallback, mDisplayDeviceConfig, mHandler);
mDisplayBrightnessController =
new DisplayBrightnessController(context, null,
@@ -1497,6 +1497,9 @@
// allowed range.
float animateValue = clampScreenBrightness(brightnessState);
+ // custom transition duration
+ float customTransitionRate = -1f;
+
// If there are any HDR layers on the screen, we have a special brightness value that we
// use instead. We still preserve the calculated brightness for Standard Dynamic Range
// (SDR) layers, but the main brightness value will be the one for HDR.
@@ -1511,6 +1514,7 @@
// We want to scale HDR brightness level with the SDR level, we also need to restore
// SDR brightness immediately when entering dim or low power mode.
animateValue = mBrightnessRangeController.getHdrBrightnessValue();
+ customTransitionRate = mBrightnessRangeController.getHdrTransitionRate();
}
final float currentBrightness = mPowerState.getScreenBrightness();
@@ -1523,6 +1527,9 @@
|| !isDisplayContentVisible || brightnessIsTemporary) {
animateScreenBrightness(animateValue, sdrAnimateValue,
SCREEN_ANIMATION_RATE_MINIMUM);
+ } else if (customTransitionRate > 0) {
+ animateScreenBrightness(animateValue, sdrAnimateValue,
+ customTransitionRate);
} else {
boolean isIncreasing = animateValue > currentBrightness;
final float rampSpeed;
@@ -2968,6 +2975,13 @@
hbmChangeCallback, hbmMetadata, context);
}
+ BrightnessRangeController getBrightnessRangeController(
+ HighBrightnessModeController hbmController, Runnable modeChangeCallback,
+ DisplayDeviceConfig displayDeviceConfig, Handler handler) {
+ return new BrightnessRangeController(hbmController,
+ modeChangeCallback, displayDeviceConfig, handler);
+ }
+
DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
SensorManager sensorManager, Resources resources) {
return DisplayWhiteBalanceFactory.create(handler,
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
new file mode 100644
index 0000000..079a196
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.clamper;
+
+import android.os.Handler;
+import android.os.PowerManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HdrClamper {
+
+ private final Configuration mConfiguration = new Configuration();
+
+ private final BrightnessClamperController.ClamperChangeListener mClamperChangeListener;
+
+ private final Handler mHandler;
+
+ private final Runnable mDebouncer;
+
+ private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+
+ // brightness change speed, in units per seconds,
+ private float mTransitionRate = -1f;
+
+ private float mDesiredMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+
+ private float mDesiredTransitionDuration = -1; // in seconds
+
+ public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+ Handler handler) {
+ mClamperChangeListener = clamperChangeListener;
+ mHandler = handler;
+ mDebouncer = () -> {
+ mTransitionRate = Math.abs((mMaxBrightness - mDesiredMaxBrightness)
+ / mDesiredTransitionDuration);
+ mMaxBrightness = mDesiredMaxBrightness;
+ mClamperChangeListener.onChanged();
+ };
+ }
+
+ // Called in same looper: mHandler.getLooper()
+ public float getMaxBrightness() {
+ return mMaxBrightness;
+ }
+
+ // Called in same looper: mHandler.getLooper()
+ public float getTransitionRate() {
+ return mTransitionRate;
+ }
+
+
+ /**
+ * Updates brightness cap in response to ambient lux change.
+ * Called by ABC in same looper: mHandler.getLooper()
+ */
+ public void onAmbientLuxChange(float ambientLux) {
+ float expectedMaxBrightness = findBrightnessLimit(ambientLux);
+ if (mMaxBrightness == expectedMaxBrightness) {
+ mDesiredMaxBrightness = mMaxBrightness;
+ mDesiredTransitionDuration = -1;
+ mTransitionRate = -1f;
+ mHandler.removeCallbacks(mDebouncer);
+ } else if (mDesiredMaxBrightness != expectedMaxBrightness) {
+ mDesiredMaxBrightness = expectedMaxBrightness;
+ long debounceTime;
+ if (mDesiredMaxBrightness > mMaxBrightness) {
+ debounceTime = mConfiguration.mIncreaseConfig.mDebounceTimeMillis;
+ mDesiredTransitionDuration =
+ (float) mConfiguration.mIncreaseConfig.mTransitionTimeMillis / 1000;
+ } else {
+ debounceTime = mConfiguration.mDecreaseConfig.mDebounceTimeMillis;
+ mDesiredTransitionDuration =
+ (float) mConfiguration.mDecreaseConfig.mTransitionTimeMillis / 1000;
+ }
+
+ mHandler.removeCallbacks(mDebouncer);
+ mHandler.postDelayed(mDebouncer, debounceTime);
+ }
+ }
+
+ @VisibleForTesting
+ Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ private float findBrightnessLimit(float ambientLux) {
+ float foundAmbientBoundary = Float.MAX_VALUE;
+ float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+ for (Map.Entry<Float, Float> brightnessPoint :
+ mConfiguration.mMaxBrightnessLimits.entrySet()) {
+ float ambientBoundary = brightnessPoint.getKey();
+ // find ambient lux upper boundary closest to current ambient lux
+ if (ambientBoundary > ambientLux && ambientBoundary < foundAmbientBoundary) {
+ foundMaxBrightness = brightnessPoint.getValue();
+ foundAmbientBoundary = ambientBoundary;
+ }
+ }
+ return foundMaxBrightness;
+ }
+
+ @VisibleForTesting
+ static class Configuration {
+ final Map<Float, Float> mMaxBrightnessLimits = new HashMap<>();
+ final TransitionConfiguration mIncreaseConfig = new TransitionConfiguration();
+
+ final TransitionConfiguration mDecreaseConfig = new TransitionConfiguration();
+ }
+
+ @VisibleForTesting
+ static class TransitionConfiguration {
+ long mDebounceTimeMillis;
+
+ long mTransitionTimeMillis;
+ }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index e7dc48e..11ff42b 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -43,6 +43,7 @@
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.display.BrightnessInfo;
import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
import android.os.Handler;
@@ -67,7 +68,9 @@
import com.android.server.am.BatteryStatsService;
import com.android.server.display.RampAnimator.DualRampAnimator;
import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.HdrClamper;
import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.display.layout.Layout;
import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
import com.android.server.policy.WindowManagerPolicy;
@@ -1175,6 +1178,26 @@
verify(mHolder.displayPowerState, times(1)).stop();
}
+ @Test
+ public void testRampRateForHdrContent() {
+ float clampedBrightness = 0.6f;
+ float transitionRate = 35.5f;
+
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+ when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
+ BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
+ when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
+ when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+ when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
+
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ verify(mHolder.animator, atLeastOnce()).animateTo(eq(clampedBrightness), anyFloat(),
+ eq(transitionRate));
+ }
+
/**
* Creates a mock and registers it to {@link LocalServices}.
*/
@@ -1270,12 +1293,16 @@
final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
mock(ScreenOffBrightnessSensorController.class);
final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
+ final HdrClamper hdrClamper = mock(HdrClamper.class);
+ final DeviceConfigParameterProvider deviceConfigParameterProvider =
+ mock(DeviceConfigParameterProvider.class);
when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
TestInjector injector = spy(new TestInjector(displayPowerState, animator,
automaticBrightnessController, wakelockController, brightnessMappingStrategy,
- hysteresisLevels, screenOffBrightnessSensorController, hbmController));
+ hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper,
+ deviceConfigParameterProvider));
final LogicalDisplay display = mock(LogicalDisplay.class);
final DisplayDevice device = mock(DisplayDevice.class);
@@ -1293,7 +1320,7 @@
return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
animator, automaticBrightnessController, wakelockController,
- screenOffBrightnessSensorController, hbmController, hbmMetadata,
+ screenOffBrightnessSensorController, hbmController, hdrClamper, hbmMetadata,
brightnessMappingStrategy, injector);
}
@@ -1311,6 +1338,8 @@
public final WakelockController wakelockController;
public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
public final HighBrightnessModeController hbmController;
+
+ public final HdrClamper hdrClamper;
public final HighBrightnessModeMetadata hbmMetadata;
public final BrightnessMappingStrategy brightnessMappingStrategy;
public final DisplayPowerController2.Injector injector;
@@ -1322,6 +1351,7 @@
WakelockController wakelockController,
ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
HighBrightnessModeController hbmController,
+ HdrClamper hdrClamper,
HighBrightnessModeMetadata hbmMetadata,
BrightnessMappingStrategy brightnessMappingStrategy,
DisplayPowerController2.Injector injector) {
@@ -1334,6 +1364,7 @@
this.wakelockController = wakelockController;
this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
this.hbmController = hbmController;
+ this.hdrClamper = hdrClamper;
this.hbmMetadata = hbmMetadata;
this.brightnessMappingStrategy = brightnessMappingStrategy;
this.injector = injector;
@@ -1350,13 +1381,19 @@
private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
private final HighBrightnessModeController mHighBrightnessModeController;
+ private final HdrClamper mHdrClamper;
+
+ private final DeviceConfigParameterProvider mDeviceConfigParameterProvider;
+
TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
AutomaticBrightnessController automaticBrightnessController,
WakelockController wakelockController,
BrightnessMappingStrategy brightnessMappingStrategy,
HysteresisLevels hysteresisLevels,
ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
- HighBrightnessModeController highBrightnessModeController) {
+ HighBrightnessModeController highBrightnessModeController,
+ HdrClamper hdrClamper,
+ DeviceConfigParameterProvider deviceConfigParameterProvider) {
mDisplayPowerState = dps;
mAnimator = animator;
mAutomaticBrightnessController = automaticBrightnessController;
@@ -1365,6 +1402,8 @@
mHysteresisLevels = hysteresisLevels;
mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
mHighBrightnessModeController = highBrightnessModeController;
+ mHdrClamper = hdrClamper;
+ mDeviceConfigParameterProvider = deviceConfigParameterProvider;
}
@Override
@@ -1471,6 +1510,14 @@
}
@Override
+ BrightnessRangeController getBrightnessRangeController(
+ HighBrightnessModeController hbmController, Runnable modeChangeCallback,
+ DisplayDeviceConfig displayDeviceConfig, Handler handler) {
+ return new BrightnessRangeController(hbmController, modeChangeCallback,
+ displayDeviceConfig, mHdrClamper, mDeviceConfigParameterProvider);
+ }
+
+ @Override
DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
SensorManager sensorManager, Resources resources) {
return mDisplayWhiteBalanceControllerMock;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
new file mode 100644
index 0000000..0ebe46a
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.clamper;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.os.PowerManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+public class HdrClamperTest {
+
+ public static final float FLOAT_TOLERANCE = 0.0001f;
+
+ @Rule
+ public MockitoRule mRule = MockitoJUnit.rule();
+
+ @Mock
+ private BrightnessClamperController.ClamperChangeListener mMockListener;
+
+ OffsettableClock mClock = new OffsettableClock.Stopped();
+
+ private final TestHandler mTestHandler = new TestHandler(null, mClock);
+
+
+ private HdrClamper mHdrClamper;
+
+
+ @Before
+ public void setUp() {
+ mHdrClamper = new HdrClamper(mMockListener, mTestHandler);
+ configureClamper();
+ }
+
+ @Test
+ public void testClamper_AmbientLuxChangesAboveLimit() {
+ mHdrClamper.onAmbientLuxChange(500);
+
+ assertFalse(mTestHandler.hasMessagesOrCallbacks());
+ assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+ }
+
+ @Test
+ public void testClamper_AmbientLuxChangesBelowLimit() {
+ mHdrClamper.onAmbientLuxChange(499);
+
+ assertTrue(mTestHandler.hasMessagesOrCallbacks());
+ TestHandler.MsgInfo msgInfo = mTestHandler.getPendingMessages().peek();
+ assertEquals(2000, msgInfo.sendTime);
+ assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+
+ mClock.fastForward(2000);
+ mTestHandler.timeAdvance();
+ assertEquals(0.6f, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+ }
+
+ @Test
+ public void testClamper_AmbientLuxChangesBelowLimit_ThenFastAboveLimit() {
+ mHdrClamper.onAmbientLuxChange(499);
+ mHdrClamper.onAmbientLuxChange(500);
+
+ assertFalse(mTestHandler.hasMessagesOrCallbacks());
+ assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+ }
+
+ @Test
+ public void testClamper_AmbientLuxChangesBelowLimit_ThenSlowlyAboveLimit() {
+ mHdrClamper.onAmbientLuxChange(499);
+ mClock.fastForward(2000);
+ mTestHandler.timeAdvance();
+
+ mHdrClamper.onAmbientLuxChange(500);
+
+ assertTrue(mTestHandler.hasMessagesOrCallbacks());
+ TestHandler.MsgInfo msgInfo = mTestHandler.getPendingMessages().peek();
+ assertEquals(3000, msgInfo.sendTime); // 2000 + 1000
+
+ mClock.fastForward(1000);
+ mTestHandler.timeAdvance();
+ assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+ }
+
+ private void configureClamper() {
+ mHdrClamper.getConfiguration().mMaxBrightnessLimits.put(500f, 0.6f);
+ mHdrClamper.getConfiguration().mIncreaseConfig.mDebounceTimeMillis = 1000;
+ mHdrClamper.getConfiguration().mIncreaseConfig.mTransitionTimeMillis = 1500;
+ mHdrClamper.getConfiguration().mDecreaseConfig.mDebounceTimeMillis = 2000;
+ mHdrClamper.getConfiguration().mDecreaseConfig.mTransitionTimeMillis = 2500;
+ }
+}