diff options
author | Lloyd Pique <lpique@google.com> | 2019-11-14 14:24:10 -0800 |
---|---|---|
committer | Lloyd Pique <lpique@google.com> | 2019-11-20 16:28:04 -0800 |
commit | 17ca742f6fd147c060080e57a714b8595f45729f (patch) | |
tree | f19234beb0b23671c34ac0975185e2bde6590ce4 /services/surfaceflinger/CompositionEngine/tests | |
parent | cb6af747af315bbafe0aa769a36ad4d5229d1954 (diff) | |
download | native-17ca742f6fd147c060080e57a714b8595f45729f.tar.gz |
CE: Unit test coverage for Output::updateColorProfile()
Since these tests are complicated, it also introduces
CallOrderStateMachineHelper, which is used to enforce the setup/execute
progression in a way that intentionally highlights what is unique about
the set up for each test case.
Bug: 144116957
Test: atest libcompositionengine_test
Change-Id: I691e1cba27ae63b051151e6d884671ce921d19ff
Diffstat (limited to 'services/surfaceflinger/CompositionEngine/tests')
-rw-r--r-- | services/surfaceflinger/CompositionEngine/tests/CallOrderStateMachineHelper.h | 126 | ||||
-rw-r--r-- | services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp | 767 |
2 files changed, 889 insertions, 4 deletions
diff --git a/services/surfaceflinger/CompositionEngine/tests/CallOrderStateMachineHelper.h b/services/surfaceflinger/CompositionEngine/tests/CallOrderStateMachineHelper.h new file mode 100644 index 0000000000..2675dcf069 --- /dev/null +++ b/services/surfaceflinger/CompositionEngine/tests/CallOrderStateMachineHelper.h @@ -0,0 +1,126 @@ +/* + * Copyright 2019 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. + */ + +#pragma once + +/** + * CallOrderStateMachineHelper is a helper class for setting up a compile-time + * checked state machine that a sequence of calls is correct for completely + * setting up the state for some other type. + * + * Two examples where this could be used are with setting up a "Builder" flow + * for initializing an instance of some type, and writing tests where the state + * machine sets up expectations and preconditions, calls the function under + * test, and then evaluations postconditions. + * + * The purpose of this helper is to offload some of the boilerplate code to + * simplify the actual state classes, and is also a place to document how to + * go about setting up the state classes. + * + * To work at compile time, the idea is that each state is a unique C++ type, + * and the valid transitions between states are given by member functions on + * those types, with those functions returning a simple value type expressing + * the new state to use. Illegal state transitions become a compile error because + * a named member function does not exist. + * + * Example usage in a test: + * + * A two step (+ terminator step) setup process can defined using: + * + * class Step1 : public CallOrderStateMachineHelper<TestFixtureType, Step1> { + * [[nodiscard]] auto firstMockCalledWith(int value1) { + * // Set up an expectation or initial state using the fixture + * EXPECT_CALL(getInstance->firstMock, FirstCall(value1)); + * return nextState<Step2>(); + * } + * }; + * + * class Step2 : public CallOrderStateMachineHelper<TestFixtureType, Step2> { + * [[nodiscard]] auto secondMockCalledWith(int value2) { + * // Set up an expectation or initial state using the fixture + * EXPECT_CALL(getInstance()->secondMock, SecondCall(value2)); + * return nextState<StepExecute>(); + * } + * }; + * + * class StepExecute : public CallOrderStateMachineHelper<TestFixtureType, Step3> { + * void execute() { + * invokeFunctionUnderTest(); + * } + * }; + * + * Note how the non-terminator steps return by value and use [[nodiscard]] to + * enforce the setup flow. Only the terminator step returns void. + * + * This can then be used in the tests with: + * + * Step1::make(this).firstMockCalledWith(value1) + * .secondMockCalledWith(value2) + * .execute); + * + * If the test fixture defines a `verify()` helper function which returns + * `Step1::make(this)`, this can be simplified to: + * + * verify().firstMockCalledWith(value1) + * .secondMockCalledWith(value2) + * .execute(); + * + * This is equivalent to the following calls made by the text function: + * + * EXPECT_CALL(firstMock, FirstCall(value1)); + * EXPECT_CALL(secondMock, SecondCall(value2)); + * invokeFunctionUnderTest(); + */ +template <typename InstanceType, typename CurrentStateType> +class CallOrderStateMachineHelper { +public: + CallOrderStateMachineHelper() = default; + + // Disallow copying + CallOrderStateMachineHelper(const CallOrderStateMachineHelper&) = delete; + CallOrderStateMachineHelper& operator=(const CallOrderStateMachineHelper&) = delete; + + // Moving is intended use case. + CallOrderStateMachineHelper(CallOrderStateMachineHelper&&) = default; + CallOrderStateMachineHelper& operator=(CallOrderStateMachineHelper&&) = default; + + // Using a static "Make" function means the CurrentStateType classes do not + // need anything other than a default no-argument constructor. + static CurrentStateType make(InstanceType* instance) { + auto helper = CurrentStateType(); + helper.mInstance = instance; + return helper; + } + + // Each non-terminal state function + template <typename NextStateType> + auto nextState() { + // Note: Further operations on the current state become undefined + // operations as the instance pointer is moved to the next state type. + // But that doesn't stop someone from storing an intermediate state + // instance as a local and possibly calling one than one member function + // on it. By swapping with nullptr, we at least can try to catch this + // this at runtime. + InstanceType* instance = nullptr; + std::swap(instance, mInstance); + return NextStateType::make(instance); + } + + InstanceType* getInstance() const { return mInstance; } + +private: + InstanceType* mInstance; +}; diff --git a/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp b/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp index 242ccd57c2..615fb94c34 100644 --- a/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp +++ b/services/surfaceflinger/CompositionEngine/tests/OutputTest.cpp @@ -16,6 +16,7 @@ #include <cmath> +#include <android-base/stringprintf.h> #include <compositionengine/LayerFECompositionState.h> #include <compositionengine/impl/Output.h> #include <compositionengine/impl/OutputCompositionState.h> @@ -31,6 +32,7 @@ #include <ui/Rect.h> #include <ui/Region.h> +#include "CallOrderStateMachineHelper.h" #include "RegionMatcher.h" #include "TransformMatcher.h" @@ -39,10 +41,13 @@ namespace { using testing::_; using testing::ByMove; +using testing::DoAll; using testing::InSequence; +using testing::Mock; using testing::Ref; using testing::Return; using testing::ReturnRef; +using testing::SetArgPointee; using testing::StrictMock; constexpr auto TR_IDENT = 0u; @@ -52,6 +57,9 @@ const mat4 kIdentity; const mat4 kNonIdentityHalf = mat4() * 0.5; const mat4 kNonIdentityQuarter = mat4() * 0.25; +constexpr OutputColorSetting kVendorSpecifiedOutputColorSetting = + static_cast<OutputColorSetting>(0x100); + struct OutputPartialMockBase : public impl::Output { // compositionengine::Output overrides const OutputCompositionState& getState() const override { return mState; } @@ -107,6 +115,30 @@ struct OutputTest : public testing::Test { const Rect OutputTest::kDefaultDisplaySize{100, 200}; +using ColorProfile = compositionengine::Output::ColorProfile; + +void dumpColorProfile(ColorProfile profile, std::string& result, const char* name) { + android::base::StringAppendF(&result, "%s (%s[%d] %s[%d] %s[%d] %s[%d]) ", name, + toString(profile.mode).c_str(), profile.mode, + toString(profile.dataspace).c_str(), profile.dataspace, + toString(profile.renderIntent).c_str(), profile.renderIntent, + toString(profile.colorSpaceAgnosticDataspace).c_str(), + profile.colorSpaceAgnosticDataspace); +} + +// Checks for a ColorProfile match +MATCHER_P(ColorProfileEq, expected, "") { + std::string buf; + buf.append("ColorProfiles are not equal\n"); + dumpColorProfile(expected, buf, "expected value"); + dumpColorProfile(arg, buf, "actual value"); + *result_listener << buf; + + return (expected.mode == arg.mode) && (expected.dataspace == arg.dataspace) && + (expected.renderIntent == arg.renderIntent) && + (expected.colorSpaceAgnosticDataspace == arg.colorSpaceAgnosticDataspace); +} + /* * Basic construction */ @@ -296,10 +328,12 @@ TEST_F(OutputTest, setColorTransformPerformsUpdateForHalfToQuarter) { } /* - * Output::setColorMode + * Output::setColorProfile */ -TEST_F(OutputTest, setColorModeSetsStateAndDirtiesOutputIfChanged) { +using OutputSetColorProfileTest = OutputTest; + +TEST_F(OutputSetColorProfileTest, setsStateAndDirtiesOutputIfChanged) { using ColorProfile = Output::ColorProfile; EXPECT_CALL(*mDisplayColorProfile, @@ -320,7 +354,7 @@ TEST_F(OutputTest, setColorModeSetsStateAndDirtiesOutputIfChanged) { EXPECT_THAT(mOutput->getState().dirtyRegion, RegionEq(Region(kDefaultDisplaySize))); } -TEST_F(OutputTest, setColorModeDoesNothingIfNoChange) { +TEST_F(OutputSetColorProfileTest, doesNothingIfNoChange) { using ColorProfile = Output::ColorProfile; EXPECT_CALL(*mDisplayColorProfile, @@ -658,7 +692,732 @@ TEST_F(OutputPresentTest, justInvokesChildFunctionsInSequence) { * Output::updateColorProfile() */ -// TODO(b/144060211) - Add coverage +struct OutputUpdateColorProfileTest : public testing::Test { + using TestType = OutputUpdateColorProfileTest; + + struct OutputPartialMock : public OutputPartialMockBase { + // All child helper functions Output::present() are defined as mocks, + // and those are tested separately, allowing the present() test to + // just cover the high level flow. + MOCK_METHOD1(setColorProfile, void(const ColorProfile&)); + }; + + struct Layer { + Layer() { + EXPECT_CALL(mOutputLayer, getLayer()).WillRepeatedly(ReturnRef(mLayer)); + EXPECT_CALL(mOutputLayer, getLayerFE()).WillRepeatedly(ReturnRef(mLayerFE)); + EXPECT_CALL(mLayer, getFEState()).WillRepeatedly(ReturnRef(mLayerFEState)); + } + + StrictMock<mock::OutputLayer> mOutputLayer; + StrictMock<mock::Layer> mLayer; + StrictMock<mock::LayerFE> mLayerFE; + LayerFECompositionState mLayerFEState; + }; + + OutputUpdateColorProfileTest() { + mOutput.setDisplayColorProfileForTest( + std::unique_ptr<DisplayColorProfile>(mDisplayColorProfile)); + mOutput.setRenderSurfaceForTest(std::unique_ptr<RenderSurface>(mRenderSurface)); + + EXPECT_CALL(mOutput, getOutputLayerOrderedByZByIndex(0)) + .WillRepeatedly(Return(&mLayer1.mOutputLayer)); + EXPECT_CALL(mOutput, getOutputLayerOrderedByZByIndex(1)) + .WillRepeatedly(Return(&mLayer2.mOutputLayer)); + EXPECT_CALL(mOutput, getOutputLayerOrderedByZByIndex(2)) + .WillRepeatedly(Return(&mLayer3.mOutputLayer)); + } + + struct ExecuteState : public CallOrderStateMachineHelper<TestType, ExecuteState> { + void execute() { getInstance()->mOutput.updateColorProfile(getInstance()->mRefreshArgs); } + }; + + mock::DisplayColorProfile* mDisplayColorProfile = new StrictMock<mock::DisplayColorProfile>(); + mock::RenderSurface* mRenderSurface = new StrictMock<mock::RenderSurface>(); + StrictMock<OutputPartialMock> mOutput; + + Layer mLayer1; + Layer mLayer2; + Layer mLayer3; + + CompositionRefreshArgs mRefreshArgs; +}; + +// TODO(b/144522012): Refactor Output::updateColorProfile and the related code +// to make it easier to write unit tests. + +TEST_F(OutputUpdateColorProfileTest, setsAColorProfileWhenUnmanaged) { + // When the outputColorSetting is set to kUnmanaged, the implementation sets + // a simple default color profile without looking at anything else. + + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(3)); + EXPECT_CALL(mOutput, + setColorProfile(ColorProfileEq( + ColorProfile{ui::ColorMode::NATIVE, ui::Dataspace::UNKNOWN, + ui::RenderIntent::COLORIMETRIC, ui::Dataspace::UNKNOWN}))); + + mRefreshArgs.outputColorSetting = OutputColorSetting::kUnmanaged; + mRefreshArgs.colorSpaceAgnosticDataspace = ui::Dataspace::UNKNOWN; + + mOutput.updateColorProfile(mRefreshArgs); +} + +struct OutputUpdateColorProfileTest_GetBestColorModeResultBecomesSetProfile + : public OutputUpdateColorProfileTest { + OutputUpdateColorProfileTest_GetBestColorModeResultBecomesSetProfile() { + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(0)); + mRefreshArgs.outputColorSetting = OutputColorSetting::kEnhanced; + mRefreshArgs.colorSpaceAgnosticDataspace = ui::Dataspace::UNKNOWN; + } + + struct ExpectBestColorModeCallResultUsedToSetColorProfileState + : public CallOrderStateMachineHelper< + TestType, ExpectBestColorModeCallResultUsedToSetColorProfileState> { + [[nodiscard]] auto expectBestColorModeCallResultUsedToSetColorProfile( + ui::ColorMode colorMode, ui::Dataspace dataspace, ui::RenderIntent renderIntent) { + EXPECT_CALL(*getInstance()->mDisplayColorProfile, + getBestColorMode(ui::Dataspace::V0_SRGB, ui::RenderIntent::ENHANCE, _, _, + _)) + .WillOnce(DoAll(SetArgPointee<2>(dataspace), SetArgPointee<3>(colorMode), + SetArgPointee<4>(renderIntent))); + EXPECT_CALL(getInstance()->mOutput, + setColorProfile( + ColorProfileEq(ColorProfile{colorMode, dataspace, renderIntent, + ui::Dataspace::UNKNOWN}))); + return nextState<ExecuteState>(); + } + }; + + // Call this member function to start using the mini-DSL defined above. + [[nodiscard]] auto verify() { + return ExpectBestColorModeCallResultUsedToSetColorProfileState::make(this); + } +}; + +TEST_F(OutputUpdateColorProfileTest_GetBestColorModeResultBecomesSetProfile, + Native_Unknown_Colorimetric_Set) { + verify().expectBestColorModeCallResultUsedToSetColorProfile(ui::ColorMode::NATIVE, + ui::Dataspace::UNKNOWN, + ui::RenderIntent::COLORIMETRIC) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_GetBestColorModeResultBecomesSetProfile, + DisplayP3_DisplayP3_Enhance_Set) { + verify().expectBestColorModeCallResultUsedToSetColorProfile(ui::ColorMode::DISPLAY_P3, + ui::Dataspace::DISPLAY_P3, + ui::RenderIntent::ENHANCE) + .execute(); +} + +struct OutputUpdateColorProfileTest_ColorSpaceAgnosticeDataspaceAffectsSetColorProfile + : public OutputUpdateColorProfileTest { + OutputUpdateColorProfileTest_ColorSpaceAgnosticeDataspaceAffectsSetColorProfile() { + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(0)); + EXPECT_CALL(*mDisplayColorProfile, + getBestColorMode(ui::Dataspace::V0_SRGB, ui::RenderIntent::ENHANCE, _, _, _)) + .WillRepeatedly(DoAll(SetArgPointee<2>(ui::Dataspace::UNKNOWN), + SetArgPointee<3>(ui::ColorMode::NATIVE), + SetArgPointee<4>(ui::RenderIntent::COLORIMETRIC))); + mRefreshArgs.outputColorSetting = OutputColorSetting::kEnhanced; + } + + struct IfColorSpaceAgnosticDataspaceSetToState + : public CallOrderStateMachineHelper<TestType, IfColorSpaceAgnosticDataspaceSetToState> { + [[nodiscard]] auto ifColorSpaceAgnosticDataspaceSetTo(ui::Dataspace dataspace) { + getInstance()->mRefreshArgs.colorSpaceAgnosticDataspace = dataspace; + return nextState<ThenExpectSetColorProfileCallUsesColorSpaceAgnosticDataspaceState>(); + } + }; + + struct ThenExpectSetColorProfileCallUsesColorSpaceAgnosticDataspaceState + : public CallOrderStateMachineHelper< + TestType, ThenExpectSetColorProfileCallUsesColorSpaceAgnosticDataspaceState> { + [[nodiscard]] auto thenExpectSetColorProfileCallUsesColorSpaceAgnosticDataspace( + ui::Dataspace dataspace) { + EXPECT_CALL(getInstance()->mOutput, + setColorProfile(ColorProfileEq( + ColorProfile{ui::ColorMode::NATIVE, ui::Dataspace::UNKNOWN, + ui::RenderIntent::COLORIMETRIC, dataspace}))); + return nextState<ExecuteState>(); + } + }; + + // Call this member function to start using the mini-DSL defined above. + [[nodiscard]] auto verify() { return IfColorSpaceAgnosticDataspaceSetToState::make(this); } +}; + +TEST_F(OutputUpdateColorProfileTest_ColorSpaceAgnosticeDataspaceAffectsSetColorProfile, DisplayP3) { + verify().ifColorSpaceAgnosticDataspaceSetTo(ui::Dataspace::DISPLAY_P3) + .thenExpectSetColorProfileCallUsesColorSpaceAgnosticDataspace(ui::Dataspace::DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_ColorSpaceAgnosticeDataspaceAffectsSetColorProfile, V0_SRGB) { + verify().ifColorSpaceAgnosticDataspaceSetTo(ui::Dataspace::V0_SRGB) + .thenExpectSetColorProfileCallUsesColorSpaceAgnosticDataspace(ui::Dataspace::V0_SRGB) + .execute(); +} + +struct OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference + : public OutputUpdateColorProfileTest { + // Internally the implementation looks through the dataspaces of all the + // visible layers. The topmost one that also has an actual dataspace + // preference set is used to drive subsequent choices. + + OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference() { + mRefreshArgs.outputColorSetting = OutputColorSetting::kEnhanced; + mRefreshArgs.colorSpaceAgnosticDataspace = ui::Dataspace::UNKNOWN; + + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(3)); + EXPECT_CALL(mOutput, setColorProfile(_)).WillRepeatedly(Return()); + } + + struct IfTopLayerDataspaceState + : public CallOrderStateMachineHelper<TestType, IfTopLayerDataspaceState> { + [[nodiscard]] auto ifTopLayerIs(ui::Dataspace dataspace) { + getInstance()->mLayer3.mLayerFEState.dataspace = dataspace; + return nextState<AndIfMiddleLayerDataspaceState>(); + } + [[nodiscard]] auto ifTopLayerHasNoPreference() { + return ifTopLayerIs(ui::Dataspace::UNKNOWN); + } + }; + + struct AndIfMiddleLayerDataspaceState + : public CallOrderStateMachineHelper<TestType, AndIfMiddleLayerDataspaceState> { + [[nodiscard]] auto andIfMiddleLayerIs(ui::Dataspace dataspace) { + getInstance()->mLayer2.mLayerFEState.dataspace = dataspace; + return nextState<AndIfBottomLayerDataspaceState>(); + } + [[nodiscard]] auto andIfMiddleLayerHasNoPreference() { + return andIfMiddleLayerIs(ui::Dataspace::UNKNOWN); + } + }; + + struct AndIfBottomLayerDataspaceState + : public CallOrderStateMachineHelper<TestType, AndIfBottomLayerDataspaceState> { + [[nodiscard]] auto andIfBottomLayerIs(ui::Dataspace dataspace) { + getInstance()->mLayer1.mLayerFEState.dataspace = dataspace; + return nextState<ThenExpectBestColorModeCallUsesState>(); + } + [[nodiscard]] auto andIfBottomLayerHasNoPreference() { + return andIfBottomLayerIs(ui::Dataspace::UNKNOWN); + } + }; + + struct ThenExpectBestColorModeCallUsesState + : public CallOrderStateMachineHelper<TestType, ThenExpectBestColorModeCallUsesState> { + [[nodiscard]] auto thenExpectBestColorModeCallUses(ui::Dataspace dataspace) { + EXPECT_CALL(*getInstance()->mDisplayColorProfile, + getBestColorMode(dataspace, _, _, _, _)); + return nextState<ExecuteState>(); + } + }; + + // Call this member function to start using the mini-DSL defined above. + [[nodiscard]] auto verify() { return IfTopLayerDataspaceState::make(this); } +}; + +TEST_F(OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference, + noStrongLayerPrefenceUses_V0_SRGB) { + // If none of the layers indicate a preference, then V0_SRGB is the + // preferred choice (subject to additional checks). + verify().ifTopLayerHasNoPreference() + .andIfMiddleLayerHasNoPreference() + .andIfBottomLayerHasNoPreference() + .thenExpectBestColorModeCallUses(ui::Dataspace::V0_SRGB) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference, + ifTopmostUses_DisplayP3_Then_DisplayP3_Chosen) { + // If only the topmost layer has a preference, then that is what is chosen. + verify().ifTopLayerIs(ui::Dataspace::DISPLAY_P3) + .andIfMiddleLayerHasNoPreference() + .andIfBottomLayerHasNoPreference() + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference, + ifMiddleUses_DisplayP3_Then_DisplayP3_Chosen) { + // If only the middle layer has a preference, that that is what is chosen. + verify().ifTopLayerHasNoPreference() + .andIfMiddleLayerIs(ui::Dataspace::DISPLAY_P3) + .andIfBottomLayerHasNoPreference() + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference, + ifBottomUses_DisplayP3_Then_DisplayP3_Chosen) { + // If only the middle layer has a preference, that that is what is chosen. + verify().ifTopLayerHasNoPreference() + .andIfMiddleLayerHasNoPreference() + .andIfBottomLayerIs(ui::Dataspace::DISPLAY_P3) + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference, + ifTopUses_DisplayBT2020_AndBottomUses_DisplayP3_Then_DisplayBT2020_Chosen) { + // If multiple layers have a preference, the topmost value is what is used. + verify().ifTopLayerIs(ui::Dataspace::DISPLAY_BT2020) + .andIfMiddleLayerHasNoPreference() + .andIfBottomLayerIs(ui::Dataspace::DISPLAY_P3) + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_BT2020) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_TopmostLayerPreferenceSetsOutputPreference, + ifTopUses_DisplayP3_AndBottomUses_V0_SRGB_Then_DisplayP3_Chosen) { + // If multiple layers have a preference, the topmost value is what is used. + verify().ifTopLayerIs(ui::Dataspace::DISPLAY_P3) + .andIfMiddleLayerHasNoPreference() + .andIfBottomLayerIs(ui::Dataspace::DISPLAY_BT2020) + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_P3) + .execute(); +} + +struct OutputUpdateColorProfileTest_ForceOutputColorOverrides + : public OutputUpdateColorProfileTest { + // If CompositionRefreshArgs::forceOutputColorMode is set to some specific + // values, it overrides the layer dataspace choice. + + OutputUpdateColorProfileTest_ForceOutputColorOverrides() { + mRefreshArgs.outputColorSetting = OutputColorSetting::kEnhanced; + mRefreshArgs.colorSpaceAgnosticDataspace = ui::Dataspace::UNKNOWN; + + mLayer1.mLayerFEState.dataspace = ui::Dataspace::DISPLAY_BT2020; + + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(1)); + EXPECT_CALL(mOutput, setColorProfile(_)).WillRepeatedly(Return()); + } + + struct IfForceOutputColorModeState + : public CallOrderStateMachineHelper<TestType, IfForceOutputColorModeState> { + [[nodiscard]] auto ifForceOutputColorMode(ui::ColorMode colorMode) { + getInstance()->mRefreshArgs.forceOutputColorMode = colorMode; + return nextState<ThenExpectBestColorModeCallUsesState>(); + } + [[nodiscard]] auto ifNoOverride() { return ifForceOutputColorMode(ui::ColorMode::NATIVE); } + }; + + struct ThenExpectBestColorModeCallUsesState + : public CallOrderStateMachineHelper<TestType, ThenExpectBestColorModeCallUsesState> { + [[nodiscard]] auto thenExpectBestColorModeCallUses(ui::Dataspace dataspace) { + EXPECT_CALL(*getInstance()->mDisplayColorProfile, + getBestColorMode(dataspace, _, _, _, _)); + return nextState<ExecuteState>(); + } + }; + + // Call this member function to start using the mini-DSL defined above. + [[nodiscard]] auto verify() { return IfForceOutputColorModeState::make(this); } +}; + +TEST_F(OutputUpdateColorProfileTest_ForceOutputColorOverrides, NoOverride_DoesNotOverride) { + // By default the layer state is used to set the preferred dataspace + verify().ifNoOverride() + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_BT2020) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_ForceOutputColorOverrides, SRGB_Override_USES_V0_SRGB) { + // Setting ui::ColorMode::SRGB overrides it with ui::Dataspace::V0_SRGB + verify().ifForceOutputColorMode(ui::ColorMode::SRGB) + .thenExpectBestColorModeCallUses(ui::Dataspace::V0_SRGB) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_ForceOutputColorOverrides, DisplayP3_Override_Uses_DisplayP3) { + // Setting ui::ColorMode::DISPLAY_P3 overrides it with ui::Dataspace::DISPLAY_P3 + verify().ifForceOutputColorMode(ui::ColorMode::DISPLAY_P3) + .thenExpectBestColorModeCallUses(ui::Dataspace::DISPLAY_P3) + .execute(); +} + +// HDR output requires all layers to be compatible with the chosen HDR +// dataspace, along with there being proper support. +struct OutputUpdateColorProfileTest_Hdr : public OutputUpdateColorProfileTest { + OutputUpdateColorProfileTest_Hdr() { + mRefreshArgs.outputColorSetting = OutputColorSetting::kEnhanced; + mRefreshArgs.colorSpaceAgnosticDataspace = ui::Dataspace::UNKNOWN; + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(2)); + EXPECT_CALL(mOutput, setColorProfile(_)).WillRepeatedly(Return()); + } + + static constexpr ui::Dataspace kNonHdrDataspace = ui::Dataspace::DISPLAY_P3; + static constexpr ui::Dataspace BT2020_PQ = ui::Dataspace::BT2020_PQ; + static constexpr ui::Dataspace BT2020_HLG = ui::Dataspace::BT2020_HLG; + static constexpr ui::Dataspace DISPLAY_P3 = ui::Dataspace::DISPLAY_P3; + + struct IfTopLayerDataspaceState + : public CallOrderStateMachineHelper<TestType, IfTopLayerDataspaceState> { + [[nodiscard]] auto ifTopLayerIs(ui::Dataspace dataspace) { + getInstance()->mLayer2.mLayerFEState.dataspace = dataspace; + return nextState<AndTopLayerCompositionTypeState>(); + } + [[nodiscard]] auto ifTopLayerIsNotHdr() { return ifTopLayerIs(kNonHdrDataspace); } + }; + + struct AndTopLayerCompositionTypeState + : public CallOrderStateMachineHelper<TestType, AndTopLayerCompositionTypeState> { + [[nodiscard]] auto andTopLayerIsREComposed(bool renderEngineComposed) { + getInstance()->mLayer2.mLayerFEState.forceClientComposition = renderEngineComposed; + return nextState<AndIfBottomLayerDataspaceState>(); + } + }; + + struct AndIfBottomLayerDataspaceState + : public CallOrderStateMachineHelper<TestType, AndIfBottomLayerDataspaceState> { + [[nodiscard]] auto andIfBottomLayerIs(ui::Dataspace dataspace) { + getInstance()->mLayer1.mLayerFEState.dataspace = dataspace; + return nextState<AndBottomLayerCompositionTypeState>(); + } + [[nodiscard]] auto andIfBottomLayerIsNotHdr() { + return andIfBottomLayerIs(kNonHdrDataspace); + } + }; + + struct AndBottomLayerCompositionTypeState + : public CallOrderStateMachineHelper<TestType, AndBottomLayerCompositionTypeState> { + [[nodiscard]] auto andBottomLayerIsREComposed(bool renderEngineComposed) { + getInstance()->mLayer1.mLayerFEState.forceClientComposition = renderEngineComposed; + return nextState<AndIfHasLegacySupportState>(); + } + }; + + struct AndIfHasLegacySupportState + : public CallOrderStateMachineHelper<TestType, AndIfHasLegacySupportState> { + [[nodiscard]] auto andIfLegacySupportFor(ui::Dataspace dataspace, bool legacySupport) { + EXPECT_CALL(*getInstance()->mDisplayColorProfile, hasLegacyHdrSupport(dataspace)) + .WillOnce(Return(legacySupport)); + return nextState<ThenExpectBestColorModeCallUsesState>(); + } + }; + + struct ThenExpectBestColorModeCallUsesState + : public CallOrderStateMachineHelper<TestType, ThenExpectBestColorModeCallUsesState> { + [[nodiscard]] auto thenExpectBestColorModeCallUses(ui::Dataspace dataspace) { + EXPECT_CALL(*getInstance()->mDisplayColorProfile, + getBestColorMode(dataspace, _, _, _, _)); + return nextState<ExecuteState>(); + } + }; + + // Call this member function to start using the mini-DSL defined above. + [[nodiscard]] auto verify() { return IfTopLayerDataspaceState::make(this); } +}; + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_PQ_HW_Uses_PQ) { + // If all layers use BT2020_PQ, and there are no other special conditions, + // BT2020_PQ is used. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_PQ_HW_IfPQHasLegacySupport_Uses_DisplayP3) { + // BT2020_PQ is not used if there is only legacy support for it. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, true) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_PQ_RE_Uses_PQ) { + // BT2020_PQ is still used if the bottom layer is RenderEngine composed. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(true) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_RE_On_PQ_HW_Uses_DisplayP3) { + // BT2020_PQ is not used if the top layer is RenderEngine composed. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(true) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_HLG_HW_Uses_PQ) { + // If there is mixed HLG/PQ use, and the topmost layer is PQ, then PQ is used if there + // are no other special conditions. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_HLG_HW_IfPQHasLegacySupport_Uses_DisplayP3) { + // BT2020_PQ is not used if there is only legacy support for it. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, true) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_HLG_RE_Uses_PQ) { + // BT2020_PQ is used if the bottom HLG layer is RenderEngine composed. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(true) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_RE_On_HLG_HW_Uses_DisplayP3) { + // BT2020_PQ is not used if the top PQ layer is RenderEngine composed. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(true) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_PQ_HW_Uses_PQ) { + // If there is mixed HLG/PQ use, and the topmost layer is HLG, then PQ is + // used if there are no other special conditions. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_PQ_HW_IfPQHasLegacySupport_Uses_DisplayP3) { + // BT2020_PQ is not used if there is only legacy support for it. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, true) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_PQ_RE_Uses_DisplayP3) { + // BT2020_PQ is not used if the bottom PQ layer is RenderEngine composed. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(true) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_RE_On_PQ_HW_Uses_PQ) { + // BT2020_PQ is still used if the top HLG layer is RenderEngine composed. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(true) + .andIfBottomLayerIs(BT2020_PQ) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_HLG_HW_Uses_HLG) { + // If all layers use HLG then HLG is used if there are no other special + // conditions. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_HLG, false) + .thenExpectBestColorModeCallUses(BT2020_HLG) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_HLG_HW_IfPQHasLegacySupport_Uses_DisplayP3) { + // BT2020_HLG is not used if there is legacy support for it. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_HLG, true) + .thenExpectBestColorModeCallUses(DISPLAY_P3) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_HLG_RE_Uses_HLG) { + // BT2020_HLG is used even if the bottom layer is client composed. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(true) + .andIfLegacySupportFor(BT2020_HLG, false) + .thenExpectBestColorModeCallUses(BT2020_HLG) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_RE_On_HLG_HW_Uses_HLG) { + // BT2020_HLG is used even if the top layer is client composed. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(true) + .andIfBottomLayerIs(BT2020_HLG) + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_HLG, false) + .thenExpectBestColorModeCallUses(BT2020_HLG) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, PQ_HW_On_NonHdr_HW_Uses_PQ) { + // Even if there are non-HDR layers present, BT2020_PQ can still be used. + verify().ifTopLayerIs(BT2020_PQ) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIsNotHdr() + .andBottomLayerIsREComposed(false) + .andIfLegacySupportFor(BT2020_PQ, false) + .thenExpectBestColorModeCallUses(BT2020_PQ) + .execute(); +} + +TEST_F(OutputUpdateColorProfileTest_Hdr, HLG_HW_On_NonHdr_RE_Uses_HLG) { + // If all layers use HLG then HLG is used if there are no other special + // conditions. + verify().ifTopLayerIs(BT2020_HLG) + .andTopLayerIsREComposed(false) + .andIfBottomLayerIsNotHdr() + .andBottomLayerIsREComposed(true) + .andIfLegacySupportFor(BT2020_HLG, false) + .thenExpectBestColorModeCallUses(BT2020_HLG) + .execute(); +} + +struct OutputUpdateColorProfile_AffectsChosenRenderIntentTest + : public OutputUpdateColorProfileTest { + // The various values for CompositionRefreshArgs::outputColorSetting affect + // the chosen renderIntent, along with whether the preferred dataspace is an + // HDR dataspace or not. + + OutputUpdateColorProfile_AffectsChosenRenderIntentTest() { + mRefreshArgs.outputColorSetting = OutputColorSetting::kEnhanced; + mRefreshArgs.colorSpaceAgnosticDataspace = ui::Dataspace::UNKNOWN; + mLayer1.mLayerFEState.dataspace = ui::Dataspace::BT2020_PQ; + EXPECT_CALL(mOutput, getOutputLayerCount()).WillRepeatedly(Return(1)); + EXPECT_CALL(mOutput, setColorProfile(_)).WillRepeatedly(Return()); + EXPECT_CALL(*mDisplayColorProfile, hasLegacyHdrSupport(ui::Dataspace::BT2020_PQ)) + .WillRepeatedly(Return(false)); + } + + // The tests here involve enough state and GMock setup that using a mini-DSL + // makes the tests much more readable, and allows the test to focus more on + // the intent than on some of the details. + + static constexpr ui::Dataspace kNonHdrDataspace = ui::Dataspace::DISPLAY_P3; + static constexpr ui::Dataspace kHdrDataspace = ui::Dataspace::BT2020_PQ; + + struct IfDataspaceChosenState + : public CallOrderStateMachineHelper<TestType, IfDataspaceChosenState> { + [[nodiscard]] auto ifDataspaceChosenIs(ui::Dataspace dataspace) { + getInstance()->mLayer1.mLayerFEState.dataspace = dataspace; + return nextState<AndOutputColorSettingState>(); + } + [[nodiscard]] auto ifDataspaceChosenIsNonHdr() { + return ifDataspaceChosenIs(kNonHdrDataspace); + } + [[nodiscard]] auto ifDataspaceChosenIsHdr() { return ifDataspaceChosenIs(kHdrDataspace); } + }; + + struct AndOutputColorSettingState + : public CallOrderStateMachineHelper<TestType, AndOutputColorSettingState> { + [[nodiscard]] auto andOutputColorSettingIs(OutputColorSetting setting) { + getInstance()->mRefreshArgs.outputColorSetting = setting; + return nextState<ThenExpectBestColorModeCallUsesState>(); + } + }; + + struct ThenExpectBestColorModeCallUsesState + : public CallOrderStateMachineHelper<TestType, ThenExpectBestColorModeCallUsesState> { + [[nodiscard]] auto thenExpectBestColorModeCallUses(ui::RenderIntent intent) { + EXPECT_CALL(*getInstance()->mDisplayColorProfile, + getBestColorMode(getInstance()->mLayer1.mLayerFEState.dataspace, intent, _, + _, _)); + return nextState<ExecuteState>(); + } + }; + + // Tests call one of these two helper member functions to start using the + // mini-DSL defined above. + [[nodiscard]] auto verify() { return IfDataspaceChosenState::make(this); } +}; + +TEST_F(OutputUpdateColorProfile_AffectsChosenRenderIntentTest, + Managed_NonHdr_Prefers_Colorimetric) { + verify().ifDataspaceChosenIsNonHdr() + .andOutputColorSettingIs(OutputColorSetting::kManaged) + .thenExpectBestColorModeCallUses(ui::RenderIntent::COLORIMETRIC) + .execute(); +} + +TEST_F(OutputUpdateColorProfile_AffectsChosenRenderIntentTest, + Managed_Hdr_Prefers_ToneMapColorimetric) { + verify().ifDataspaceChosenIsHdr() + .andOutputColorSettingIs(OutputColorSetting::kManaged) + .thenExpectBestColorModeCallUses(ui::RenderIntent::TONE_MAP_COLORIMETRIC) + .execute(); +} + +TEST_F(OutputUpdateColorProfile_AffectsChosenRenderIntentTest, Enhanced_NonHdr_Prefers_Enhance) { + verify().ifDataspaceChosenIsNonHdr() + .andOutputColorSettingIs(OutputColorSetting::kEnhanced) + .thenExpectBestColorModeCallUses(ui::RenderIntent::ENHANCE) + .execute(); +} + +TEST_F(OutputUpdateColorProfile_AffectsChosenRenderIntentTest, + Enhanced_Hdr_Prefers_ToneMapEnhance) { + verify().ifDataspaceChosenIsHdr() + .andOutputColorSettingIs(OutputColorSetting::kEnhanced) + .thenExpectBestColorModeCallUses(ui::RenderIntent::TONE_MAP_ENHANCE) + .execute(); +} + +TEST_F(OutputUpdateColorProfile_AffectsChosenRenderIntentTest, Vendor_NonHdr_Prefers_Vendor) { + verify().ifDataspaceChosenIsNonHdr() + .andOutputColorSettingIs(kVendorSpecifiedOutputColorSetting) + .thenExpectBestColorModeCallUses( + static_cast<ui::RenderIntent>(kVendorSpecifiedOutputColorSetting)) + .execute(); +} + +TEST_F(OutputUpdateColorProfile_AffectsChosenRenderIntentTest, Vendor_Hdr_Prefers_Vendor) { + verify().ifDataspaceChosenIsHdr() + .andOutputColorSettingIs(kVendorSpecifiedOutputColorSetting) + .thenExpectBestColorModeCallUses( + static_cast<ui::RenderIntent>(kVendorSpecifiedOutputColorSetting)) + .execute(); +} /* * Output::beginFrame() |